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