1 package Koha::Calendar;
8 use DateTime::Duration;
14 my ( $classname, %options ) = @_;
16 bless $self, $classname;
17 for my $o_name ( keys %options ) {
19 $self->{$o} = $options{$o_name};
21 if ( !defined $self->{branchcode} ) {
22 croak 'No branchcode argument passed to Koha::Calendar->new';
30 my $branch = $self->{branchcode};
31 my $dbh = C4::Context->dbh();
32 my $weekly_closed_days_sth = $dbh->prepare(
33 'SELECT weekday FROM repeatable_holidays WHERE branchcode = ? AND weekday IS NOT NULL'
35 $weekly_closed_days_sth->execute( $branch );
36 $self->{weekly_closed_days} = [ 0, 0, 0, 0, 0, 0, 0 ];
37 while ( my $tuple = $weekly_closed_days_sth->fetchrow_hashref ) {
38 $self->{weekly_closed_days}->[ $tuple->{weekday} ] = 1;
40 my $day_month_closed_days_sth = $dbh->prepare(
41 'SELECT day, month FROM repeatable_holidays WHERE branchcode = ? AND weekday IS NULL'
43 $day_month_closed_days_sth->execute( $branch );
44 $self->{day_month_closed_days} = {};
45 while ( my $tuple = $day_month_closed_days_sth->fetchrow_hashref ) {
46 $self->{day_month_closed_days}->{ $tuple->{month} }->{ $tuple->{day} } =
50 $self->{days_mode} ||= C4::Context->preference('useDaysMode');
55 sub exception_holidays {
58 my $branch = $self->{branchcode};
59 my $cache = Koha::Caches->get_instance();
60 my $key = 'exception_holidays_'.$branch;
61 my $cached = $cache->get_from_cache($key);
62 return $cached if $cached;
64 my $dbh = C4::Context->dbh;
65 my $exception_holidays_sth = $dbh->prepare(
66 'SELECT day, month, year FROM special_holidays WHERE branchcode = ? AND isexception = 1'
68 $exception_holidays_sth->execute( $branch );
70 while ( my ( $day, $month, $year ) = $exception_holidays_sth->fetchrow ) {
76 time_zone => "floating",
77 )->truncate( to => 'day' );
79 $self->{exception_holidays} =
80 DateTime::Set->from_datetimes( dates => $dates );
81 $cache->set_in_cache( $key, $self->{exception_holidays} );
82 return $self->{exception_holidays};
86 my ( $self, $date ) = @_;
87 my $branchcode = $self->{branchcode};
88 my $cache = Koha::Caches->get_instance();
89 my $single_holidays = $cache->get_from_cache('single_holidays');
91 # $single_holidays looks like:
100 unless ($single_holidays) {
101 my $dbh = C4::Context->dbh;
102 $single_holidays = {};
104 # push holidays for each branch
106 $dbh->prepare('SELECT distinct(branchcode) FROM special_holidays');
107 $branches_sth->execute();
108 while ( my $br = $branches_sth->fetchrow ) {
109 my $single_holidays_sth = $dbh->prepare(
110 'SELECT day, month, year FROM special_holidays WHERE branchcode = ? AND isexception = 0'
112 $single_holidays_sth->execute($br);
115 while ( my ( $day, $month, $year ) =
116 $single_holidays_sth->fetchrow )
118 my $dt = DateTime->new(
122 time_zone => 'floating',
123 )->truncate( to => 'day' );
124 push @ymd_arr, $dt->ymd('');
126 $single_holidays->{$br} = \@ymd_arr;
128 $cache->set_in_cache( 'single_holidays', $single_holidays,
129 { expiry => 76800 } ) #24 hrs ;
131 my $holidays = ( $single_holidays->{$branchcode} );
132 for my $hols (@$holidays ) {
133 return 1 if ( $date == $hols ) #match ymds;
139 my ( $self, $startdate, $add_duration, $unit ) = @_;
141 # Default to days duration (legacy support I guess)
142 if ( ref $add_duration ne 'DateTime::Duration' ) {
143 $add_duration = DateTime::Duration->new( days => $add_duration );
146 $unit ||= 'days'; # default days ?
149 if ( $unit eq 'hours' ) {
150 # Fixed for legacy support. Should be set as a branch parameter
151 my $return_by_hour = 10;
153 $dt = $self->addHours($startdate, $add_duration, $return_by_hour);
156 $dt = $self->addDays($startdate, $add_duration);
163 my ( $self, $startdate, $hours_duration, $return_by_hour ) = @_;
164 my $base_date = $startdate->clone();
166 $base_date->add_duration($hours_duration);
168 # If we are using the calendar behave for now as if Datedue
169 # was the chosen option (current intended behaviour)
171 if ( $self->{days_mode} ne 'Days' &&
172 $self->is_holiday($base_date) ) {
174 if ( $hours_duration->is_negative() ) {
175 $base_date = $self->prev_open_day($base_date);
177 $base_date = $self->next_open_day($base_date);
180 $base_date->set_hour($return_by_hour);
188 my ( $self, $startdate, $days_duration ) = @_;
189 my $base_date = $startdate->clone();
191 $self->{days_mode} ||= q{};
193 if ( $self->{days_mode} eq 'Calendar' ) {
194 # use the calendar to skip all days the library is closed
196 my $days = abs $days_duration->in_units('days');
198 if ( $days_duration->is_negative() ) {
200 $base_date = $self->prev_open_day($base_date);
205 $base_date = $self->next_open_day($base_date);
210 } else { # Days or Datedue
211 # use straight days, then use calendar to push
212 # the date to the next open day if Datedue
213 $base_date->add_duration($days_duration);
215 if ( $self->{days_mode} eq 'Datedue' ) {
216 # Datedue, then use the calendar to push
217 # the date to the next open day if holiday
218 if ( $self->is_holiday($base_date) ) {
220 if ( $days_duration->is_negative() ) {
221 $base_date = $self->prev_open_day($base_date);
223 $base_date = $self->next_open_day($base_date);
233 my ( $self, $dt ) = @_;
235 my $localdt = $dt->clone();
236 my $day = $localdt->day;
237 my $month = $localdt->month;
239 #Change timezone to "floating" before doing any calculations or comparisons
240 $localdt->set_time_zone("floating");
241 $localdt->truncate( to => 'day' );
244 if ( $self->exception_holidays->contains($localdt) ) {
245 # exceptions are not holidays
249 my $dow = $localdt->day_of_week;
251 # DateTime object dow (1-7) where Monday is 1
252 # Arrays are 0-based where 0 = Sunday, not 7.
257 if ( $self->{weekly_closed_days}->[$dow] == 1 ) {
261 if ( exists $self->{day_month_closed_days}->{$month}->{$day} ) {
265 my $ymd = $localdt->ymd('') ;
266 if ($self->single_holidays( $ymd ) == 1 ) {
270 # damn have to go to work after all
275 my ( $self, $dt ) = @_;
276 my $base_date = $dt->clone();
278 $base_date->add(days => 1);
280 while ($self->is_holiday($base_date)) {
281 $base_date->add(days => 1);
288 my ( $self, $dt ) = @_;
289 my $base_date = $dt->clone();
291 $base_date->add(days => -1);
293 while ($self->is_holiday($base_date)) {
294 $base_date->add(days => -1);
302 my $start_dt = shift;
303 my $num_days = shift;
305 return $start_dt unless $num_days > 0;
307 my $base_dt = $start_dt->clone();
309 while ($num_days--) {
310 $base_dt = $self->next_open_day($base_dt);
318 my $start_dt = shift;
321 # Change time zone for date math and swap if needed
322 $start_dt = $start_dt->clone->set_time_zone('floating');
323 $end_dt = $end_dt->clone->set_time_zone('floating');
324 if( $start_dt->compare($end_dt) > 0 ) {
325 ( $start_dt, $end_dt ) = ( $end_dt, $start_dt );
328 # start and end should not be closed days
329 my $days = $start_dt->delta_days($end_dt)->delta_days;
330 while( $start_dt->compare($end_dt) < 1 ) {
331 $days-- if $self->is_holiday($start_dt);
332 $start_dt->add( days => 1 );
334 return DateTime::Duration->new( days => $days );
338 my ($self, $start_date, $end_date) = @_;
339 my $start_dt = $start_date->clone()->set_time_zone('floating');
340 my $end_dt = $end_date->clone()->set_time_zone('floating');
341 my $duration = $end_dt->delta_ms($start_dt);
342 $start_dt->truncate( to => 'day' );
343 $end_dt->truncate( to => 'day' );
344 # NB this is a kludge in that it assumes all days are 24 hours
345 # However for hourly loans the logic should be expanded to
346 # take into account open/close times then it would be a duration
347 # of library open hours
348 my $skipped_days = 0;
349 for (my $dt = $start_dt->clone();
353 if ($self->is_holiday($dt)) {
358 $duration->subtract_duration(DateTime::Duration->new( hours => 24 * $skipped_days));
366 my ( $self, $mode ) = @_;
368 # if not testing this is a no op
369 if ( $self->{test} ) {
370 $self->{days_mode} = $mode;
376 sub clear_weekly_closed_days {
378 $self->{weekly_closed_days} = [ 0, 0, 0, 0, 0, 0, 0 ]; # Sunday only
387 Koha::Calendar - Object containing a branches calendar
393 my $c = Koha::Calendar->new( branchcode => 'MAIN' );
394 my $dt = DateTime->now();
397 $open = $c->is_holiday($dt);
398 # when will item be due if loan period = $dur (a DateTime::Duration object)
399 $duedate = $c->addDate($dt,$dur,'days');
404 Implements those features of C4::Calendar needed for Staffs Rolling Loans
408 =head2 new : Create a calendar object
410 my $calendar = Koha::Calendar->new( branchcode => 'MAIN' );
412 The option branchcode is required
417 my $dt = $calendar->addDate($date, $dur, $unit)
419 C<$date> is a DateTime object representing the starting date of the interval.
421 C<$offset> is a DateTime::Duration to add to it
423 C<$unit> is a string value 'days' or 'hours' toflag granularity of duration
425 Currently unit is only used to invoke Staffs return Monday at 10 am rule this
426 parameter will be removed when issuingrules properly cope with that
431 my $dt = $calendar->addHours($date, $dur, $return_by_hour )
433 C<$date> is a DateTime object representing the starting date of the interval.
435 C<$offset> is a DateTime::Duration to add to it
437 C<$return_by_hour> is an integer value representing the opening hour for the branch
442 my $dt = $calendar->addDays($date, $dur)
444 C<$date> is a DateTime object representing the starting date of the interval.
446 C<$offset> is a DateTime::Duration to add to it
448 C<$unit> is a string value 'days' or 'hours' toflag granularity of duration
450 Currently unit is only used to invoke Staffs return Monday at 10 am rule this
451 parameter will be removed when issuingrules properly cope with that
454 =head2 single_holidays
456 my $rc = $self->single_holidays( $ymd );
458 Passed a $date in Ymd (yyyymmdd) format - returns 1 if date is a single_holiday, or 0 if not.
463 $yesno = $calendar->is_holiday($dt);
465 passed a DateTime object returns 1 if it is a closed day
466 0 if not according to the calendar
470 $duration = $calendar->days_between($start_dt, $end_dt);
472 Passed two dates returns a DateTime::Duration object measuring the length between them
473 ignoring closed days. Always returns a positive number irrespective of the
474 relative order of the parameters
478 $datetime = $calendar->next_open_day($duedate_dt)
480 Passed a Datetime returns another Datetime representing the next open day. It is
481 intended for use to calculate the due date when useDaysMode syspref is set to either
482 'Datedue' or 'Calendar'.
486 $datetime = $calendar->prev_open_day($duedate_dt)
488 Passed a Datetime returns another Datetime representing the previous open day. It is
489 intended for use to calculate the due date when useDaysMode syspref is set to either
490 'Datedue' or 'Calendar'.
494 For testing only allows the calling script to change days mode
496 =head2 clear_weekly_closed_days
498 In test mode changes the testing set of closed days to a new set with
499 no closed days. TODO passing an array of closed days to this would
500 allow testing of more configurations
504 Passed a datetime object this will add it to the calendar's list of
505 closed days. This is for testing so that we can alter the Calenfar object's
506 list of specified dates
510 Will croak if not passed a branchcode in new
512 =head1 BUGS AND LIMITATIONS
514 This only contains a limited subset of the functionality in C4::Calendar
515 Only enough to support Staffs Rolling loans
519 Colin Campbell colin.campbell@ptfs-europe.com
521 =head1 LICENSE AND COPYRIGHT
523 Copyright (c) 2011 PTFS-Europe Ltd All rights reserved
525 This program is free software: you can redistribute it and/or modify
526 it under the terms of the GNU General Public License as published by
527 the Free Software Foundation, either version 2 of the License, or
528 (at your option) any later version.
530 This program is distributed in the hope that it will be useful,
531 but WITHOUT ANY WARRANTY; without even the implied warranty of
532 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
533 GNU General Public License for more details.
535 You should have received a copy of the GNU General Public License
536 along with this program. If not, see <http://www.gnu.org/licenses/>.