"Fossies" - the Fresh Open Source Software Archive

Member "koha-19.11.15/Koha/Calendar.pm" (23 Feb 2021, 17111 Bytes) of package /linux/misc/koha-19.11.15.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Perl source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "Calendar.pm" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 20.05.06_vs_20.11.00.

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