Lines 1-535
Link Here
|
1 |
package Koha::Calendar; |
|
|
2 |
use strict; |
3 |
use warnings; |
4 |
use 5.010; |
5 |
|
6 |
use DateTime; |
7 |
use DateTime::Set; |
8 |
use DateTime::Duration; |
9 |
use C4::Context; |
10 |
use Koha::Caches; |
11 |
use Carp; |
12 |
|
13 |
sub new { |
14 |
my ( $classname, %options ) = @_; |
15 |
my $self = {}; |
16 |
bless $self, $classname; |
17 |
for my $o_name ( keys %options ) { |
18 |
my $o = lc $o_name; |
19 |
$self->{$o} = $options{$o_name}; |
20 |
} |
21 |
if ( !defined $self->{branchcode} ) { |
22 |
croak 'No branchcode argument passed to Koha::Calendar->new'; |
23 |
} |
24 |
$self->_init(); |
25 |
return $self; |
26 |
} |
27 |
|
28 |
sub _init { |
29 |
my $self = shift; |
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' |
34 |
); |
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; |
39 |
} |
40 |
my $day_month_closed_days_sth = $dbh->prepare( |
41 |
'SELECT day, month FROM repeatable_holidays WHERE branchcode = ? AND weekday IS NULL' |
42 |
); |
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} } = |
47 |
1; |
48 |
} |
49 |
|
50 |
$self->{days_mode} ||= C4::Context->preference('useDaysMode'); |
51 |
$self->{test} = 0; |
52 |
return; |
53 |
} |
54 |
|
55 |
sub exception_holidays { |
56 |
my ( $self ) = @_; |
57 |
|
58 |
my $cache = Koha::Caches->get_instance(); |
59 |
my $cached = $cache->get_from_cache('exception_holidays'); |
60 |
return $cached if $cached; |
61 |
|
62 |
my $dbh = C4::Context->dbh; |
63 |
my $branch = $self->{branchcode}; |
64 |
my $exception_holidays_sth = $dbh->prepare( |
65 |
'SELECT day, month, year FROM special_holidays WHERE branchcode = ? AND isexception = 1' |
66 |
); |
67 |
$exception_holidays_sth->execute( $branch ); |
68 |
my $dates = []; |
69 |
while ( my ( $day, $month, $year ) = $exception_holidays_sth->fetchrow ) { |
70 |
push @{$dates}, |
71 |
DateTime->new( |
72 |
day => $day, |
73 |
month => $month, |
74 |
year => $year, |
75 |
time_zone => "floating", |
76 |
)->truncate( to => 'day' ); |
77 |
} |
78 |
$self->{exception_holidays} = |
79 |
DateTime::Set->from_datetimes( dates => $dates ); |
80 |
$cache->set_in_cache( 'exception_holidays', $self->{exception_holidays} ); |
81 |
return $self->{exception_holidays}; |
82 |
} |
83 |
|
84 |
sub single_holidays { |
85 |
my ( $self, $date ) = @_; |
86 |
my $branchcode = $self->{branchcode}; |
87 |
my $cache = Koha::Caches->get_instance(); |
88 |
my $single_holidays = $cache->get_from_cache('single_holidays'); |
89 |
|
90 |
# $single_holidays looks like: |
91 |
# { |
92 |
# CPL => [ |
93 |
# [0] 20131122, |
94 |
# ... |
95 |
# ], |
96 |
# ... |
97 |
# } |
98 |
|
99 |
unless ($single_holidays) { |
100 |
my $dbh = C4::Context->dbh; |
101 |
$single_holidays = {}; |
102 |
|
103 |
# push holidays for each branch |
104 |
my $branches_sth = |
105 |
$dbh->prepare('SELECT distinct(branchcode) FROM special_holidays'); |
106 |
$branches_sth->execute(); |
107 |
while ( my $br = $branches_sth->fetchrow ) { |
108 |
my $single_holidays_sth = $dbh->prepare( |
109 |
'SELECT day, month, year FROM special_holidays WHERE branchcode = ? AND isexception = 0' |
110 |
); |
111 |
$single_holidays_sth->execute($br); |
112 |
|
113 |
my @ymd_arr; |
114 |
while ( my ( $day, $month, $year ) = |
115 |
$single_holidays_sth->fetchrow ) |
116 |
{ |
117 |
my $dt = DateTime->new( |
118 |
day => $day, |
119 |
month => $month, |
120 |
year => $year, |
121 |
time_zone => 'floating', |
122 |
)->truncate( to => 'day' ); |
123 |
push @ymd_arr, $dt->ymd(''); |
124 |
} |
125 |
$single_holidays->{$br} = \@ymd_arr; |
126 |
} # br |
127 |
$cache->set_in_cache( 'single_holidays', $single_holidays, |
128 |
{ expiry => 76800 } ) #24 hrs ; |
129 |
} |
130 |
my $holidays = ( $single_holidays->{$branchcode} ); |
131 |
for my $hols (@$holidays ) { |
132 |
return 1 if ( $date == $hols ) #match ymds; |
133 |
} |
134 |
return 0; |
135 |
} |
136 |
|
137 |
sub addDate { |
138 |
my ( $self, $startdate, $add_duration, $unit ) = @_; |
139 |
|
140 |
# Default to days duration (legacy support I guess) |
141 |
if ( ref $add_duration ne 'DateTime::Duration' ) { |
142 |
$add_duration = DateTime::Duration->new( days => $add_duration ); |
143 |
} |
144 |
|
145 |
$unit ||= 'days'; # default days ? |
146 |
my $dt; |
147 |
|
148 |
if ( $unit eq 'hours' ) { |
149 |
# Fixed for legacy support. Should be set as a branch parameter |
150 |
my $return_by_hour = 10; |
151 |
|
152 |
$dt = $self->addHours($startdate, $add_duration, $return_by_hour); |
153 |
} else { |
154 |
# days |
155 |
$dt = $self->addDays($startdate, $add_duration); |
156 |
} |
157 |
|
158 |
return $dt; |
159 |
} |
160 |
|
161 |
sub addHours { |
162 |
my ( $self, $startdate, $hours_duration, $return_by_hour ) = @_; |
163 |
my $base_date = $startdate->clone(); |
164 |
|
165 |
$base_date->add_duration($hours_duration); |
166 |
|
167 |
# If we are using the calendar behave for now as if Datedue |
168 |
# was the chosen option (current intended behaviour) |
169 |
|
170 |
if ( $self->{days_mode} ne 'Days' && |
171 |
$self->is_holiday($base_date) ) { |
172 |
|
173 |
if ( $hours_duration->is_negative() ) { |
174 |
$base_date = $self->prev_open_day($base_date); |
175 |
} else { |
176 |
$base_date = $self->next_open_day($base_date); |
177 |
} |
178 |
|
179 |
$base_date->set_hour($return_by_hour); |
180 |
|
181 |
} |
182 |
|
183 |
return $base_date; |
184 |
} |
185 |
|
186 |
sub addDays { |
187 |
my ( $self, $startdate, $days_duration ) = @_; |
188 |
my $base_date = $startdate->clone(); |
189 |
|
190 |
$self->{days_mode} ||= q{}; |
191 |
|
192 |
if ( $self->{days_mode} eq 'Calendar' ) { |
193 |
# use the calendar to skip all days the library is closed |
194 |
# when adding |
195 |
my $days = abs $days_duration->in_units('days'); |
196 |
|
197 |
if ( $days_duration->is_negative() ) { |
198 |
while ($days) { |
199 |
$base_date = $self->prev_open_day($base_date); |
200 |
--$days; |
201 |
} |
202 |
} else { |
203 |
while ($days) { |
204 |
$base_date = $self->next_open_day($base_date); |
205 |
--$days; |
206 |
} |
207 |
} |
208 |
|
209 |
} else { # Days or Datedue |
210 |
# use straight days, then use calendar to push |
211 |
# the date to the next open day if Datedue |
212 |
$base_date->add_duration($days_duration); |
213 |
|
214 |
if ( $self->{days_mode} eq 'Datedue' ) { |
215 |
# Datedue, then use the calendar to push |
216 |
# the date to the next open day if holiday |
217 |
if ( $self->is_holiday($base_date) ) { |
218 |
|
219 |
if ( $days_duration->is_negative() ) { |
220 |
$base_date = $self->prev_open_day($base_date); |
221 |
} else { |
222 |
$base_date = $self->next_open_day($base_date); |
223 |
} |
224 |
} |
225 |
} |
226 |
} |
227 |
|
228 |
return $base_date; |
229 |
} |
230 |
|
231 |
sub is_holiday { |
232 |
my ( $self, $dt ) = @_; |
233 |
|
234 |
my $localdt = $dt->clone(); |
235 |
my $day = $localdt->day; |
236 |
my $month = $localdt->month; |
237 |
|
238 |
#Change timezone to "floating" before doing any calculations or comparisons |
239 |
$localdt->set_time_zone("floating"); |
240 |
$localdt->truncate( to => 'day' ); |
241 |
|
242 |
|
243 |
if ( $self->exception_holidays->contains($localdt) ) { |
244 |
# exceptions are not holidays |
245 |
return 0; |
246 |
} |
247 |
|
248 |
my $dow = $localdt->day_of_week; |
249 |
# Representation fix |
250 |
# DateTime object dow (1-7) where Monday is 1 |
251 |
# Arrays are 0-based where 0 = Sunday, not 7. |
252 |
if ( $dow == 7 ) { |
253 |
$dow = 0; |
254 |
} |
255 |
|
256 |
if ( $self->{weekly_closed_days}->[$dow] == 1 ) { |
257 |
return 1; |
258 |
} |
259 |
|
260 |
if ( exists $self->{day_month_closed_days}->{$month}->{$day} ) { |
261 |
return 1; |
262 |
} |
263 |
|
264 |
my $ymd = $localdt->ymd('') ; |
265 |
if ($self->single_holidays( $ymd ) == 1 ) { |
266 |
return 1; |
267 |
} |
268 |
|
269 |
# damn have to go to work after all |
270 |
return 0; |
271 |
} |
272 |
|
273 |
sub next_open_day { |
274 |
my ( $self, $dt ) = @_; |
275 |
my $base_date = $dt->clone(); |
276 |
|
277 |
$base_date->add(days => 1); |
278 |
|
279 |
while ($self->is_holiday($base_date)) { |
280 |
$base_date->add(days => 1); |
281 |
} |
282 |
|
283 |
return $base_date; |
284 |
} |
285 |
|
286 |
sub prev_open_day { |
287 |
my ( $self, $dt ) = @_; |
288 |
my $base_date = $dt->clone(); |
289 |
|
290 |
$base_date->add(days => -1); |
291 |
|
292 |
while ($self->is_holiday($base_date)) { |
293 |
$base_date->add(days => -1); |
294 |
} |
295 |
|
296 |
return $base_date; |
297 |
} |
298 |
|
299 |
sub days_forward { |
300 |
my $self = shift; |
301 |
my $start_dt = shift; |
302 |
my $num_days = shift; |
303 |
|
304 |
return $start_dt unless $num_days > 0; |
305 |
|
306 |
my $base_dt = $start_dt->clone(); |
307 |
|
308 |
while ($num_days--) { |
309 |
$base_dt = $self->next_open_day($base_dt); |
310 |
} |
311 |
|
312 |
return $base_dt; |
313 |
} |
314 |
|
315 |
sub days_between { |
316 |
my $self = shift; |
317 |
my $start_dt = shift; |
318 |
my $end_dt = shift; |
319 |
|
320 |
# Change time zone for date math and swap if needed |
321 |
$start_dt = $start_dt->clone->set_time_zone('floating'); |
322 |
$end_dt = $end_dt->clone->set_time_zone('floating'); |
323 |
if( $start_dt->compare($end_dt) > 0 ) { |
324 |
( $start_dt, $end_dt ) = ( $end_dt, $start_dt ); |
325 |
} |
326 |
|
327 |
# start and end should not be closed days |
328 |
my $days = $start_dt->delta_days($end_dt)->delta_days; |
329 |
while( $start_dt->compare($end_dt) < 1 ) { |
330 |
$days-- if $self->is_holiday($start_dt); |
331 |
$start_dt->add( days => 1 ); |
332 |
} |
333 |
return DateTime::Duration->new( days => $days ); |
334 |
} |
335 |
|
336 |
sub hours_between { |
337 |
my ($self, $start_date, $end_date) = @_; |
338 |
my $start_dt = $start_date->clone()->set_time_zone('floating'); |
339 |
my $end_dt = $end_date->clone()->set_time_zone('floating'); |
340 |
my $duration = $end_dt->delta_ms($start_dt); |
341 |
$start_dt->truncate( to => 'day' ); |
342 |
$end_dt->truncate( to => 'day' ); |
343 |
# NB this is a kludge in that it assumes all days are 24 hours |
344 |
# However for hourly loans the logic should be expanded to |
345 |
# take into account open/close times then it would be a duration |
346 |
# of library open hours |
347 |
my $skipped_days = 0; |
348 |
for (my $dt = $start_dt->clone(); |
349 |
$dt <= $end_dt; |
350 |
$dt->add(days => 1) |
351 |
) { |
352 |
if ($self->is_holiday($dt)) { |
353 |
++$skipped_days; |
354 |
} |
355 |
} |
356 |
if ($skipped_days) { |
357 |
$duration->subtract_duration(DateTime::Duration->new( hours => 24 * $skipped_days)); |
358 |
} |
359 |
|
360 |
return $duration; |
361 |
|
362 |
} |
363 |
|
364 |
sub set_daysmode { |
365 |
my ( $self, $mode ) = @_; |
366 |
|
367 |
# if not testing this is a no op |
368 |
if ( $self->{test} ) { |
369 |
$self->{days_mode} = $mode; |
370 |
} |
371 |
|
372 |
return; |
373 |
} |
374 |
|
375 |
sub clear_weekly_closed_days { |
376 |
my $self = shift; |
377 |
$self->{weekly_closed_days} = [ 0, 0, 0, 0, 0, 0, 0 ]; # Sunday only |
378 |
return; |
379 |
} |
380 |
|
381 |
1; |
382 |
__END__ |
383 |
|
384 |
=head1 NAME |
385 |
|
386 |
Koha::Calendar - Object containing a branches calendar |
387 |
|
388 |
=head1 SYNOPSIS |
389 |
|
390 |
use Koha::Calendar |
391 |
|
392 |
my $c = Koha::Calendar->new( branchcode => 'MAIN' ); |
393 |
my $dt = DateTime->now(); |
394 |
|
395 |
# are we open |
396 |
$open = $c->is_holiday($dt); |
397 |
# when will item be due if loan period = $dur (a DateTime::Duration object) |
398 |
$duedate = $c->addDate($dt,$dur,'days'); |
399 |
|
400 |
|
401 |
=head1 DESCRIPTION |
402 |
|
403 |
Implements those features of C4::Calendar needed for Staffs Rolling Loans |
404 |
|
405 |
=head1 METHODS |
406 |
|
407 |
=head2 new : Create a calendar object |
408 |
|
409 |
my $calendar = Koha::Calendar->new( branchcode => 'MAIN' ); |
410 |
|
411 |
The option branchcode is required |
412 |
|
413 |
|
414 |
=head2 addDate |
415 |
|
416 |
my $dt = $calendar->addDate($date, $dur, $unit) |
417 |
|
418 |
C<$date> is a DateTime object representing the starting date of the interval. |
419 |
|
420 |
C<$offset> is a DateTime::Duration to add to it |
421 |
|
422 |
C<$unit> is a string value 'days' or 'hours' toflag granularity of duration |
423 |
|
424 |
Currently unit is only used to invoke Staffs return Monday at 10 am rule this |
425 |
parameter will be removed when issuingrules properly cope with that |
426 |
|
427 |
|
428 |
=head2 addHours |
429 |
|
430 |
my $dt = $calendar->addHours($date, $dur, $return_by_hour ) |
431 |
|
432 |
C<$date> is a DateTime object representing the starting date of the interval. |
433 |
|
434 |
C<$offset> is a DateTime::Duration to add to it |
435 |
|
436 |
C<$return_by_hour> is an integer value representing the opening hour for the branch |
437 |
|
438 |
|
439 |
=head2 addDays |
440 |
|
441 |
my $dt = $calendar->addDays($date, $dur) |
442 |
|
443 |
C<$date> is a DateTime object representing the starting date of the interval. |
444 |
|
445 |
C<$offset> is a DateTime::Duration to add to it |
446 |
|
447 |
C<$unit> is a string value 'days' or 'hours' toflag granularity of duration |
448 |
|
449 |
Currently unit is only used to invoke Staffs return Monday at 10 am rule this |
450 |
parameter will be removed when issuingrules properly cope with that |
451 |
|
452 |
|
453 |
=head2 single_holidays |
454 |
|
455 |
my $rc = $self->single_holidays( $ymd ); |
456 |
|
457 |
Passed a $date in Ymd (yyyymmdd) format - returns 1 if date is a single_holiday, or 0 if not. |
458 |
|
459 |
|
460 |
=head2 is_holiday |
461 |
|
462 |
$yesno = $calendar->is_holiday($dt); |
463 |
|
464 |
passed a DateTime object returns 1 if it is a closed day |
465 |
0 if not according to the calendar |
466 |
|
467 |
=head2 days_between |
468 |
|
469 |
$duration = $calendar->days_between($start_dt, $end_dt); |
470 |
|
471 |
Passed two dates returns a DateTime::Duration object measuring the length between them |
472 |
ignoring closed days. Always returns a positive number irrespective of the |
473 |
relative order of the parameters |
474 |
|
475 |
=head2 next_open_day |
476 |
|
477 |
$datetime = $calendar->next_open_day($duedate_dt) |
478 |
|
479 |
Passed a Datetime returns another Datetime representing the next open day. It is |
480 |
intended for use to calculate the due date when useDaysMode syspref is set to either |
481 |
'Datedue' or 'Calendar'. |
482 |
|
483 |
=head2 prev_open_day |
484 |
|
485 |
$datetime = $calendar->prev_open_day($duedate_dt) |
486 |
|
487 |
Passed a Datetime returns another Datetime representing the previous open day. It is |
488 |
intended for use to calculate the due date when useDaysMode syspref is set to either |
489 |
'Datedue' or 'Calendar'. |
490 |
|
491 |
=head2 set_daysmode |
492 |
|
493 |
For testing only allows the calling script to change days mode |
494 |
|
495 |
=head2 clear_weekly_closed_days |
496 |
|
497 |
In test mode changes the testing set of closed days to a new set with |
498 |
no closed days. TODO passing an array of closed days to this would |
499 |
allow testing of more configurations |
500 |
|
501 |
=head2 add_holiday |
502 |
|
503 |
Passed a datetime object this will add it to the calendar's list of |
504 |
closed days. This is for testing so that we can alter the Calenfar object's |
505 |
list of specified dates |
506 |
|
507 |
=head1 DIAGNOSTICS |
508 |
|
509 |
Will croak if not passed a branchcode in new |
510 |
|
511 |
=head1 BUGS AND LIMITATIONS |
512 |
|
513 |
This only contains a limited subset of the functionality in C4::Calendar |
514 |
Only enough to support Staffs Rolling loans |
515 |
|
516 |
=head1 AUTHOR |
517 |
|
518 |
Colin Campbell colin.campbell@ptfs-europe.com |
519 |
|
520 |
=head1 LICENSE AND COPYRIGHT |
521 |
|
522 |
Copyright (c) 2011 PTFS-Europe Ltd All rights reserved |
523 |
|
524 |
This program is free software: you can redistribute it and/or modify |
525 |
it under the terms of the GNU General Public License as published by |
526 |
the Free Software Foundation, either version 2 of the License, or |
527 |
(at your option) any later version. |
528 |
|
529 |
This program is distributed in the hope that it will be useful, |
530 |
but WITHOUT ANY WARRANTY; without even the implied warranty of |
531 |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
532 |
GNU General Public License for more details. |
533 |
|
534 |
You should have received a copy of the GNU General Public License |
535 |
along with this program. If not, see <http://www.gnu.org/licenses/>. |