Bug 3274: Patch to fix Holiday Exceptions radio button.
[koha.git] / C4 / Calendar.pm
1 package C4::Calendar;
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 2 of the License, or (at your option) any later
8 # version.
9 #
10 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
16 # Suite 330, Boston, MA  02111-1307 USA
17
18 use strict;
19 use warnings;
20 use vars qw($VERSION @EXPORT);
21
22 use Carp;
23 use Date::Calc qw( Date_to_Days );
24
25 use C4::Context;
26
27 BEGIN {
28     # set the version for version checking
29     $VERSION = 3.01;
30     require Exporter;
31     @EXPORT = qw(
32         &get_week_days_holidays
33         &get_day_month_holidays
34         &get_exception_holidays 
35         &get_single_holidays
36         &insert_week_day_holiday
37         &insert_day_month_holiday
38         &insert_single_holiday
39         &insert_exception_holiday
40         &delete_holiday
41         &isHoliday
42         &addDate
43         &daysBetween
44     );
45 }
46
47 =head1 NAME
48
49 C4::Calendar::Calendar - Koha module dealing with holidays.
50
51 =head1 SYNOPSIS
52
53     use C4::Calendar::Calendar;
54
55 =head1 DESCRIPTION
56
57 This package is used to deal with holidays. Through this package, you can set all kind of holidays for the library.
58
59 =head1 FUNCTIONS
60
61 =over 2
62
63 =item new
64
65     $calendar = C4::Calendar->new(branchcode => $branchcode);
66
67 Each library branch has its own Calendar.  
68 C<$branchcode> specifies which Calendar you want.
69
70 =cut
71
72 sub new {
73     my $classname = shift @_;
74     my %options = @_;
75     my $self = bless({}, $classname);
76     foreach my $optionName (keys %options) {
77         $self->{lc($optionName)} = $options{$optionName};
78     }
79     defined($self->{branchcode}) or croak "No branchcode argument to new.  Should be C4::Calendar->new(branchcode => \$branchcode)";
80     $self->_init($self->{branchcode});
81     return $self;
82 }
83
84 sub _init {
85     my $self = shift @_;
86     my $branch = shift;
87     defined($branch) or die "No branchcode sent to _init";  # must test for defined here and above to allow ""
88     my $dbh = C4::Context->dbh();
89     my $repeatable = $dbh->prepare( 'SELECT *
90                                        FROM repeatable_holidays
91                                       WHERE ( branchcode = ? )
92                                         AND (ISNULL(weekday) = ?)' );
93     $repeatable->execute($branch,0);
94     my %week_days_holidays;
95     while (my $row = $repeatable->fetchrow_hashref) {
96         my $key = $row->{weekday};
97         $week_days_holidays{$key}{title}       = $row->{title};
98         $week_days_holidays{$key}{description} = $row->{description};
99     }
100     $self->{'week_days_holidays'} = \%week_days_holidays;
101
102     $repeatable->execute($branch,1);
103     my %day_month_holidays;
104     while (my $row = $repeatable->fetchrow_hashref) {
105         my $key = $row->{month} . "/" . $row->{day};
106         $day_month_holidays{$key}{title}       = $row->{title};
107         $day_month_holidays{$key}{description} = $row->{description};
108         $day_month_holidays{$key}{day} = sprintf("%02d", $row->{day});
109         $day_month_holidays{$key}{month} = sprintf("%02d", $row->{month});
110     }
111     $self->{'day_month_holidays'} = \%day_month_holidays;
112
113     my $special = $dbh->prepare( 'SELECT day, month, year, title, description
114                                     FROM special_holidays
115                                    WHERE ( branchcode = ? )
116                                      AND (isexception = ?)' );
117     $special->execute($branch,1);
118     my %exception_holidays;
119     while (my ($day, $month, $year, $title, $description) = $special->fetchrow) {
120         $exception_holidays{"$year/$month/$day"}{title} = $title;
121         $exception_holidays{"$year/$month/$day"}{description} = $description;
122         $exception_holidays{"$year/$month/$day"}{date} = 
123                 sprintf("%04d-%02d-%02d", $year, $month, $day);
124     }
125     $self->{'exception_holidays'} = \%exception_holidays;
126
127     $special->execute($branch,0);
128     my %single_holidays;
129     while (my ($day, $month, $year, $title, $description) = $special->fetchrow) {
130         $single_holidays{"$year/$month/$day"}{title} = $title;
131         $single_holidays{"$year/$month/$day"}{description} = $description;
132         $single_holidays{"$year/$month/$day"}{date} = 
133                 sprintf("%04d-%02d-%02d", $year, $month, $day);
134     }
135     $self->{'single_holidays'} = \%single_holidays;
136     return $self;
137 }
138
139 =item get_week_days_holidays
140
141     $week_days_holidays = $calendar->get_week_days_holidays();
142
143 Returns a hash reference to week days holidays.
144
145 =cut
146
147 sub get_week_days_holidays {
148     my $self = shift @_;
149     my $week_days_holidays = $self->{'week_days_holidays'};
150     return $week_days_holidays;
151 }
152
153 =item get_day_month_holidays
154     
155     $day_month_holidays = $calendar->get_day_month_holidays();
156
157 Returns a hash reference to day month holidays.
158
159 =cut
160
161 sub get_day_month_holidays {
162     my $self = shift @_;
163     my $day_month_holidays = $self->{'day_month_holidays'};
164     return $day_month_holidays;
165 }
166
167 =item get_exception_holidays
168     
169     $exception_holidays = $calendar->exception_holidays();
170
171 Returns a hash reference to exception holidays. This kind of days are those
172 which stands for a holiday, but you wanted to make an exception for this particular
173 date.
174
175 =cut
176
177 sub get_exception_holidays {
178     my $self = shift @_;
179     my $exception_holidays = $self->{'exception_holidays'};
180     return $exception_holidays;
181 }
182
183 =item get_single_holidays
184     
185     $single_holidays = $calendar->get_single_holidays();
186
187 Returns a hash reference to single holidays. This kind of holidays are those which
188 happend just one time.
189
190 =cut
191
192 sub get_single_holidays {
193     my $self = shift @_;
194     my $single_holidays = $self->{'single_holidays'};
195     return $single_holidays;
196 }
197
198 =item insert_week_day_holiday
199
200     insert_week_day_holiday(weekday => $weekday,
201                             title => $title,
202                             description => $description);
203
204 Inserts a new week day for $self->{branchcode}.
205
206 C<$day> Is the week day to make holiday.
207
208 C<$title> Is the title to store for the holiday formed by $year/$month/$day.
209
210 C<$description> Is the description to store for the holiday formed by $year/$month/$day.
211
212 =cut
213
214 sub insert_week_day_holiday {
215     my $self = shift @_;
216     my %options = @_;
217
218     my $dbh = C4::Context->dbh();
219     my $insertHoliday = $dbh->prepare("insert into repeatable_holidays (id,branchcode,weekday,day,month,title,description) values ( '',?,?,NULL,NULL,?,? )"); 
220         $insertHoliday->execute( $self->{branchcode}, $options{weekday},$options{title}, $options{description});
221     $self->{'week_days_holidays'}->{$options{weekday}}{title} = $options{title};
222     $self->{'week_days_holidays'}->{$options{weekday}}{description} = $options{description};
223     return $self;
224 }
225
226 =item insert_day_month_holiday
227
228     insert_day_month_holiday(day => $day,
229                              month => $month,
230                              title => $title,
231                              description => $description);
232
233 Inserts a new day month holiday for $self->{branchcode}.
234
235 C<$day> Is the day month to make the date to insert.
236
237 C<$month> Is month to make the date to insert.
238
239 C<$title> Is the title to store for the holiday formed by $year/$month/$day.
240
241 C<$description> Is the description to store for the holiday formed by $year/$month/$day.
242
243 =cut
244
245 sub insert_day_month_holiday {
246     my $self = shift @_;
247     my %options = @_;
248
249     my $dbh = C4::Context->dbh();
250     my $insertHoliday = $dbh->prepare("insert into repeatable_holidays (id,branchcode,weekday,day,month,title,description) values ('', ?, NULL, ?, ?, ?,? )");
251         $insertHoliday->execute( $self->{branchcode}, $options{day},$options{month},$options{title}, $options{description});
252     $self->{'day_month_holidays'}->{"$options{month}/$options{day}"}{title} = $options{title};
253     $self->{'day_month_holidays'}->{"$options{month}/$options{day}"}{description} = $options{description};
254     return $self;
255 }
256
257 =item insert_single_holiday
258
259     insert_single_holiday(day => $day,
260                           month => $month,
261                           year => $year,
262                           title => $title,
263                           description => $description);
264
265 Inserts a new single holiday for $self->{branchcode}.
266
267 C<$day> Is the day month to make the date to insert.
268
269 C<$month> Is month to make the date to insert.
270
271 C<$year> Is year to make the date to insert.
272
273 C<$title> Is the title to store for the holiday formed by $year/$month/$day.
274
275 C<$description> Is the description to store for the holiday formed by $year/$month/$day.
276
277 =cut
278
279 sub insert_single_holiday {
280     my $self = shift @_;
281     my %options = @_;
282     
283         my $dbh = C4::Context->dbh();
284     my $isexception = 0;
285     my $insertHoliday = $dbh->prepare("insert into special_holidays (id,branchcode,day,month,year,isexception,title,description) values ('', ?,?,?,?,?,?,?)");
286         $insertHoliday->execute( $self->{branchcode}, $options{day},$options{month},$options{year}, $isexception, $options{title}, $options{description});
287     $self->{'single_holidays'}->{"$options{year}/$options{month}/$options{day}"}{title} = $options{title};
288     $self->{'single_holidays'}->{"$options{year}/$options{month}/$options{day}"}{description} = $options{description};
289     return $self;
290 }
291
292 =item insert_exception_holiday
293
294     insert_exception_holiday(day => $day,
295                              month => $month,
296                              year => $year,
297                              title => $title,
298                              description => $description);
299
300 Inserts a new exception holiday for $self->{branchcode}.
301
302 C<$day> Is the day month to make the date to insert.
303
304 C<$month> Is month to make the date to insert.
305
306 C<$year> Is year to make the date to insert.
307
308 C<$title> Is the title to store for the holiday formed by $year/$month/$day.
309
310 C<$description> Is the description to store for the holiday formed by $year/$month/$day.
311
312 =cut
313
314 sub insert_exception_holiday {
315     my $self = shift @_;
316     my %options = @_;
317
318     my $dbh = C4::Context->dbh();
319     my $isexception = 1;
320     my $insertException = $dbh->prepare("insert into special_holidays (id,branchcode,day,month,year,isexception,title,description) values ('', ?,?,?,?,?,?,?)");
321         $insertException->execute( $self->{branchcode}, $options{day},$options{month},$options{year}, $isexception, $options{title}, $options{description});
322     $self->{'exception_holidays'}->{"$options{year}/$options{month}/$options{day}"}{title} = $options{title};
323     $self->{'exception_holidays'}->{"$options{year}/$options{month}/$options{day}"}{description} = $options{description};
324     return $self;
325 }
326
327 =item delete_holiday
328
329     delete_holiday(weekday => $weekday
330                    day => $day,
331                    month => $month,
332                    year => $year);
333
334 Delete a holiday for $self->{branchcode}.
335
336 C<$weekday> Is the week day to delete.
337
338 C<$day> Is the day month to make the date to delete.
339
340 C<$month> Is month to make the date to delete.
341
342 C<$year> Is year to make the date to delete.
343
344 =cut
345
346 sub delete_holiday {
347     my $self = shift @_;
348     my %options = @_;
349
350     # Verify what kind of holiday that day is. For example, if it is
351     # a repeatable holiday, this should check if there are some exception
352         # for that holiday rule. Otherwise, if it is a regular holiday, it´s 
353     # ok just deleting it.
354
355     my $dbh = C4::Context->dbh();
356     my $isSingleHoliday = $dbh->prepare("SELECT id FROM special_holidays WHERE (branchcode = ?) AND (day = ?) AND (month = ?) AND (year = ?)");
357     $isSingleHoliday->execute($self->{branchcode}, $options{day}, $options{month}, $options{year});
358     if ($isSingleHoliday->rows) {
359         my $id = $isSingleHoliday->fetchrow;
360         $isSingleHoliday->finish; # Close the last query
361
362         my $deleteHoliday = $dbh->prepare("DELETE FROM special_holidays WHERE id = ?");
363         $deleteHoliday->execute($id);
364         delete($self->{'single_holidays'}->{"$options{year}/$options{month}/$options{day}"});
365     } else {
366         $isSingleHoliday->finish; # Close the last query
367
368         my $isWeekdayHoliday = $dbh->prepare("SELECT id FROM repeatable_holidays WHERE branchcode = ? AND weekday = ?");
369         $isWeekdayHoliday->execute($self->{branchcode}, $options{weekday});
370         if ($isWeekdayHoliday->rows) {
371             my $id = $isWeekdayHoliday->fetchrow;
372             $isWeekdayHoliday->finish; # Close the last query
373
374             my $updateExceptions = $dbh->prepare("UPDATE special_holidays SET isexception = 0 WHERE (WEEKDAY(CONCAT(special_holidays.year,'-',special_holidays.month,'-',special_holidays.day)) = ?) AND (branchcode = ?)");
375             $updateExceptions->execute($options{weekday}, $self->{branchcode});
376             $updateExceptions->finish; # Close the last query
377
378             my $deleteHoliday = $dbh->prepare("DELETE FROM repeatable_holidays WHERE id = ?");
379             $deleteHoliday->execute($id);
380             delete($self->{'week_days_holidays'}->{$options{weekday}});
381         } else {
382             $isWeekdayHoliday->finish; # Close the last query
383
384             my $isDayMonthHoliday = $dbh->prepare("SELECT id FROM repeatable_holidays WHERE (branchcode = ?) AND (day = ?) AND (month = ?)");
385             $isDayMonthHoliday->execute($self->{branchcode}, $options{day}, $options{month});
386             if ($isDayMonthHoliday->rows) {
387                 my $id = $isDayMonthHoliday->fetchrow;
388                 $isDayMonthHoliday->finish;
389                 my $updateExceptions = $dbh->prepare("UPDATE special_holidays SET isexception = 0 WHERE (special_holidays.branchcode = ?) AND (special_holidays.day = ?) and (special_holidays.month = ?)");
390                 $updateExceptions->execute($self->{branchcode}, $options{day}, $options{month});
391                 $updateExceptions->finish; # Close the last query
392
393                 my $deleteHoliday = $dbh->prepare("DELETE FROM repeatable_holidays WHERE (id = ?)");
394                 $deleteHoliday->execute($id);
395                 delete($self->{'day_month_holidays'}->{"$options{month}/$options{day}"});
396             }
397         }
398     }
399     return $self;
400 }
401
402 =item isHoliday
403     
404     $isHoliday = isHoliday($day, $month $year);
405
406
407 C<$day> Is the day to check whether if is a holiday or not.
408
409 C<$month> Is the month to check whether if is a holiday or not.
410
411 C<$year> Is the year to check whether if is a holiday or not.
412
413 =cut
414
415 sub isHoliday {
416     my ($self, $day, $month, $year) = @_;
417         # FIXME - date strings are stored in non-padded metric format. should change to iso.
418         # FIXME - should change arguments to accept C4::Dates object
419         $month=$month+0;
420         $year=$year+0;
421         $day=$day+0;
422     my $weekday = &Date::Calc::Day_of_Week($year, $month, $day) % 7; 
423     my $weekDays   = $self->get_week_days_holidays();
424     my $dayMonths  = $self->get_day_month_holidays();
425     my $exceptions = $self->get_exception_holidays();
426     my $singles    = $self->get_single_holidays();
427     if (defined($exceptions->{"$year/$month/$day"})) {
428         return 0;
429     } else {
430         if ((exists($weekDays->{$weekday})) ||
431             (exists($dayMonths->{"$month/$day"})) ||
432             (exists($singles->{"$year/$month/$day"}))) {
433                         return 1;
434         } else {
435             return 0;
436         }
437     }
438
439 }
440
441 =item addDate
442
443     my ($day, $month, $year) = $calendar->addDate($date, $offset)
444
445 C<$date> is a C4::Dates object representing the starting date of the interval.
446
447 C<$offset> Is the number of days that this function has to count from $date.
448
449 =cut
450
451 sub addDate {
452     my ($self, $startdate, $offset) = @_;
453     my ($year,$month,$day) = split("-",$startdate->output('iso'));
454         my $daystep = 1;
455         if ($offset < 0) { # In case $offset is negative
456        # $offset = $offset*(-1);
457                 $daystep = -1;
458     }
459         my $daysMode = C4::Context->preference('useDaysMode');
460     if ($daysMode eq 'Datedue') {
461         ($year, $month, $day) = &Date::Calc::Add_Delta_Days($year, $month, $day, $offset );
462                 while ($self->isHoliday($day, $month, $year)) {
463             ($year, $month, $day) = &Date::Calc::Add_Delta_Days($year, $month, $day, $daystep);
464         }
465     } elsif($daysMode eq 'Calendar') {
466         while ($offset !=  0) {
467             ($year, $month, $day) = &Date::Calc::Add_Delta_Days($year, $month, $day, $daystep);
468             if (!($self->isHoliday($day, $month, $year))) {
469                 $offset = $offset - $daystep;
470                         }
471         }
472         } else { ## ($daysMode eq 'Days') 
473         ($year, $month, $day) = &Date::Calc::Add_Delta_Days($year, $month, $day, $offset );
474     }
475     return(C4::Dates->new( sprintf("%04d-%02d-%02d",$year,$month,$day),'iso'));
476 }
477
478 =item daysBetween
479
480     my $daysBetween = $calendar->daysBetween($startdate, $enddate)
481
482 C<$startdate> and C<$enddate> are C4::Dates objects that define the interval.
483
484 Returns the number of non-holiday days in the interval.
485 useDaysMode syspref has no effect here.
486 =cut
487
488 sub daysBetween ($$$) {
489     my $self      = shift or return undef;
490     my $startdate = shift or return undef;
491     my $enddate   = shift or return undef;
492         my ($yearFrom,$monthFrom,$dayFrom) = split("-",$startdate->output('iso'));
493         my ($yearTo,  $monthTo,  $dayTo  ) = split("-",  $enddate->output('iso'));
494         if (Date_to_Days($yearFrom,$monthFrom,$dayFrom) > Date_to_Days($yearTo,$monthTo,$dayTo)) {
495                 return 0;
496                 # we don't go backwards  ( FIXME - handle this error better )
497         }
498     my $count = 0;
499     while (1) {
500         ($yearFrom != $yearTo or $monthFrom != $monthTo or $dayFrom != $dayTo) or last; # if they all match, it's the last day
501         unless ($self->isHoliday($dayFrom, $monthFrom, $yearFrom)) {
502             $count++;
503         }
504         ($yearFrom, $monthFrom, $dayFrom) = &Date::Calc::Add_Delta_Days($yearFrom, $monthFrom, $dayFrom, 1);
505     }
506     return($count);
507 }
508
509 1;
510
511 __END__
512
513 =back
514
515 =head1 AUTHOR
516
517 Koha Physics Library UNLP <matias_veleda@hotmail.com>
518
519 =cut