875 lines
27 KiB
Perl
875 lines
27 KiB
Perl
package Koha::Library::Calendar;
|
|
|
|
use Modern::Perl;
|
|
|
|
use Carp qw( croak );
|
|
use DateTime;
|
|
use DateTime::Duration;
|
|
use C4::Context;
|
|
use Koha::Caches;
|
|
use Koha::Database;
|
|
use Koha::DateUtils qw( dt_from_string );
|
|
use Koha::Exceptions;
|
|
use Koha::Exceptions::Calendar;
|
|
use Koha::Library::Calendar::WeeklyClosures;
|
|
use Koha::Library::Calendar::RepeatingClosures;
|
|
use Koha::Library::Calendar::SingleClosures;
|
|
use Koha::Library::Calendar::Exceptions;
|
|
|
|
# This limit avoids an infinite loop when searching for an open day in an
|
|
# always closed library
|
|
# The value is arbitrary, but it should be large enough to be able to
|
|
# consider there is no open days if we haven't found any with that many
|
|
# iterations, and small enough to allow the loop to end quickly
|
|
# See next_open_days and prev_open_days
|
|
use constant OPEN_DAYS_SEARCH_MAX_ITERATIONS => 5000;
|
|
|
|
sub new {
|
|
my ( $classname, %options ) = @_;
|
|
my $self = {};
|
|
bless $self, $classname;
|
|
for my $o_name ( keys %options ) {
|
|
my $o = lc $o_name;
|
|
$self->{$o} = $options{$o_name};
|
|
}
|
|
if ( !defined $self->{branchcode} ) {
|
|
croak 'No branchcode argument passed to Koha::Library::Calendar->new';
|
|
}
|
|
$self->_init();
|
|
return $self;
|
|
}
|
|
|
|
sub _init {
|
|
my $self = shift;
|
|
my $branch = $self->{branchcode};
|
|
|
|
$self->{weekly_closed_days} = [ 0, 0, 0, 0, 0, 0, 0 ];
|
|
my $weekly = Koha::Library::Calendar::WeeklyClosures->search( { library_id => $branch } );
|
|
while ( my $row = $weekly->next ) {
|
|
$self->{weekly_closed_days}->[ $row->weekday ] = 1;
|
|
}
|
|
|
|
$self->{day_month_closed_days} = {};
|
|
my $repeating = Koha::Library::Calendar::RepeatingClosures->search( { library_id => $branch } );
|
|
while ( my $row = $repeating->next ) {
|
|
$self->{day_month_closed_days}->{ $row->month }->{ $row->day } = 1;
|
|
}
|
|
|
|
$self->{test} = 0;
|
|
return;
|
|
}
|
|
|
|
sub _holidays {
|
|
my ($self) = @_;
|
|
|
|
my $key = $self->{branchcode} . "_holidays";
|
|
my $cache = Koha::Caches->get_instance();
|
|
my $holidays = $cache->get_from_cache($key);
|
|
|
|
# $holidays looks like:
|
|
# {
|
|
# 20131122 => 1, # single_closure
|
|
# 20131123 => 0, # exception (open override)
|
|
# ...
|
|
# }
|
|
|
|
unless ($holidays) {
|
|
$holidays = {};
|
|
|
|
my $single = Koha::Library::Calendar::SingleClosures->search( { library_id => $self->{branchcode} } );
|
|
while ( my $row = $single->next ) {
|
|
( my $datestring = $row->date ) =~ s/-//g;
|
|
$holidays->{$datestring} = 1;
|
|
}
|
|
|
|
my $exceptions = Koha::Library::Calendar::Exceptions->search( { library_id => $self->{branchcode} } );
|
|
while ( my $row = $exceptions->next ) {
|
|
( my $datestring = $row->date ) =~ s/-//g;
|
|
$holidays->{$datestring} = 0;
|
|
}
|
|
|
|
$cache->set_in_cache( $key, $holidays, { expiry => 76800 } );
|
|
}
|
|
return $holidays // {};
|
|
}
|
|
|
|
sub addDuration {
|
|
my ( $self, $startdate, $add_duration, $unit ) = @_;
|
|
|
|
Koha::Exceptions::MissingParameter->throw("Missing mandatory option for Koha:Calendar->addDuration: days_mode")
|
|
unless exists $self->{days_mode};
|
|
|
|
# Default to days duration (legacy support I guess)
|
|
if ( ref $add_duration ne 'DateTime::Duration' ) {
|
|
$add_duration = DateTime::Duration->new( days => $add_duration );
|
|
}
|
|
|
|
$unit ||= 'days'; # default days ?
|
|
my $dt;
|
|
if ( $unit eq 'hours' ) {
|
|
|
|
# Fixed for legacy support. Should be set as a branch parameter
|
|
my $return_by_hour = 10;
|
|
|
|
$dt = $self->addHours( $startdate, $add_duration, $return_by_hour );
|
|
} else {
|
|
|
|
# days
|
|
$dt = $self->addDays( $startdate, $add_duration );
|
|
}
|
|
return $dt;
|
|
}
|
|
|
|
sub addHours {
|
|
my ( $self, $startdate, $hours_duration, $return_by_hour ) = @_;
|
|
my $base_date = $startdate->clone();
|
|
|
|
$base_date->add_duration($hours_duration);
|
|
|
|
# If we are using the calendar behave for now as if Datedue
|
|
# was the chosen option (current intended behaviour)
|
|
|
|
Koha::Exceptions::MissingParameter->throw("Missing mandatory option for Koha:Calendar->addHours: days_mode")
|
|
unless exists $self->{days_mode};
|
|
|
|
if ( $self->{days_mode} ne 'Days'
|
|
&& $self->is_holiday($base_date) )
|
|
{
|
|
|
|
if ( $hours_duration->is_negative() ) {
|
|
$base_date = $self->prev_open_days( $base_date, 1 );
|
|
} else {
|
|
$base_date = $self->next_open_days( $base_date, 1 );
|
|
}
|
|
|
|
$base_date->set_hour($return_by_hour);
|
|
|
|
}
|
|
|
|
return $base_date;
|
|
}
|
|
|
|
sub addDays {
|
|
my ( $self, $startdate, $days_duration ) = @_;
|
|
my $base_date = $startdate->clone();
|
|
|
|
Koha::Exceptions::MissingParameter->throw("Missing mandatory option for Koha:Calendar->addDays: days_mode")
|
|
unless exists $self->{days_mode};
|
|
|
|
if ( $self->{days_mode} eq 'Calendar' ) {
|
|
|
|
# use the calendar to skip all days the library is closed
|
|
# when adding
|
|
my $days = abs $days_duration->in_units('days');
|
|
|
|
if ( $days_duration->is_negative() ) {
|
|
while ($days) {
|
|
$base_date = $self->prev_open_days( $base_date, 1 );
|
|
--$days;
|
|
}
|
|
} else {
|
|
while ($days) {
|
|
$base_date = $self->next_open_days( $base_date, 1 );
|
|
--$days;
|
|
}
|
|
}
|
|
|
|
} else { # Days, Datedue or Dayweek
|
|
# use straight days, then use calendar to push
|
|
# the date to the next open day as appropriate
|
|
# if Datedue or Dayweek
|
|
$base_date->add_duration($days_duration);
|
|
|
|
if ( $self->{days_mode} eq 'Datedue'
|
|
|| $self->{days_mode} eq 'Dayweek' )
|
|
{
|
|
# Datedue or Dayweek, then use the calendar to push
|
|
# the date to the next open day if holiday
|
|
if ( $self->is_holiday($base_date) ) {
|
|
my $dow = $base_date->day_of_week;
|
|
my $days = $days_duration->in_units('days');
|
|
|
|
# Is it a period based on weeks
|
|
my $push_amt = $days % 7 == 0 ? $self->get_push_amt($base_date) : 1;
|
|
if ( $days_duration->is_negative() ) {
|
|
$base_date = $self->prev_open_days( $base_date, $push_amt );
|
|
} else {
|
|
$base_date = $self->next_open_days( $base_date, $push_amt );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $base_date;
|
|
}
|
|
|
|
sub get_push_amt {
|
|
my ( $self, $base_date ) = @_;
|
|
|
|
Koha::Exceptions::MissingParameter->throw("Missing mandatory option for Koha:Calendar->get_push_amt: days_mode")
|
|
unless exists $self->{days_mode};
|
|
|
|
my $dow = $base_date->day_of_week;
|
|
|
|
# Representation fix
|
|
# DateTime object dow (1-7) where Monday is 1
|
|
# Arrays are 0-based where 0 = Sunday, not 7.
|
|
if ( $dow == 7 ) {
|
|
$dow = 0;
|
|
}
|
|
|
|
return (
|
|
# We're using Dayweek useDaysMode option
|
|
$self->{days_mode} eq 'Dayweek' &&
|
|
|
|
# It's not a permanently closed day
|
|
!$self->{weekly_closed_days}->[$dow]
|
|
) ? 7 : 1;
|
|
}
|
|
|
|
sub is_holiday {
|
|
my ( $self, $dt ) = @_;
|
|
|
|
my $localdt = $dt->clone();
|
|
my $day = $localdt->day;
|
|
my $month = $localdt->month;
|
|
my $ymd = $localdt->ymd('');
|
|
|
|
#Change timezone to "floating" before doing any calculations or comparisons
|
|
$localdt->set_time_zone("floating");
|
|
$localdt->truncate( to => 'day' );
|
|
|
|
return $self->_holidays->{$ymd} if defined( $self->_holidays->{$ymd} );
|
|
|
|
my $dow = $localdt->day_of_week;
|
|
|
|
# Representation fix
|
|
# DateTime object dow (1-7) where Monday is 1
|
|
# Arrays are 0-based where 0 = Sunday, not 7.
|
|
if ( $dow == 7 ) {
|
|
$dow = 0;
|
|
}
|
|
|
|
if ( $self->{weekly_closed_days}->[$dow] == 1 ) {
|
|
return 1;
|
|
}
|
|
|
|
if ( exists $self->{day_month_closed_days}->{$month}->{$day} ) {
|
|
return 1;
|
|
}
|
|
|
|
# damn have to go to work after all
|
|
return 0;
|
|
}
|
|
|
|
sub next_open_days {
|
|
my ( $self, $dt, $to_add ) = @_;
|
|
|
|
Koha::Exceptions::MissingParameter->throw("Missing mandatory option for Koha:Calendar->next_open_days: days_mode")
|
|
unless exists $self->{days_mode};
|
|
|
|
my $base_date = $dt->clone();
|
|
|
|
$base_date->add( days => $to_add );
|
|
my $i = 0;
|
|
while ( $self->is_holiday($base_date) && $i < OPEN_DAYS_SEARCH_MAX_ITERATIONS ) {
|
|
my $add_next = $self->get_push_amt($base_date);
|
|
$base_date->add( days => $add_next );
|
|
++$i;
|
|
}
|
|
|
|
if ( $self->is_holiday($base_date) ) {
|
|
Koha::Exceptions::Calendar::NoOpenDays->throw(
|
|
sprintf( 'Unable to find an open day for library %s', $self->{branchcode} ) );
|
|
}
|
|
|
|
return $base_date;
|
|
}
|
|
|
|
sub prev_open_days {
|
|
my ( $self, $dt, $to_sub ) = @_;
|
|
|
|
Koha::Exceptions::MissingParameter->throw("Missing mandatory option for Koha:Calendar->get_open_days: days_mode")
|
|
unless exists $self->{days_mode};
|
|
|
|
my $base_date = $dt->clone();
|
|
|
|
# It feels logical to be passed a positive number, though we're
|
|
# subtracting, so do the right thing
|
|
$to_sub = $to_sub > 0 ? 0 - $to_sub : $to_sub;
|
|
|
|
$base_date->add( days => $to_sub );
|
|
|
|
my $i = 0;
|
|
while ( $self->is_holiday($base_date) && $i < OPEN_DAYS_SEARCH_MAX_ITERATIONS ) {
|
|
my $sub_next = $self->get_push_amt($base_date);
|
|
|
|
# Ensure we're subtracting when we need to be
|
|
$sub_next = $sub_next > 0 ? 0 - $sub_next : $sub_next;
|
|
$base_date->add( days => $sub_next );
|
|
++$i;
|
|
}
|
|
|
|
if ( $self->is_holiday($base_date) ) {
|
|
Koha::Exceptions::Calendar::NoOpenDays->throw(
|
|
sprintf( 'Unable to find an open day for library %s', $self->{branchcode} ) );
|
|
}
|
|
|
|
return $base_date;
|
|
}
|
|
|
|
sub days_forward {
|
|
my $self = shift;
|
|
my $start_dt = shift;
|
|
my $num_days = shift;
|
|
|
|
Koha::Exceptions::MissingParameter->throw("Missing mandatory option for Koha:Calendar->days_forward: days_mode")
|
|
unless exists $self->{days_mode};
|
|
|
|
return $start_dt unless $num_days > 0;
|
|
|
|
my $base_dt = $start_dt->clone();
|
|
|
|
while ( $num_days-- ) {
|
|
$base_dt = $self->next_open_days( $base_dt, 1 );
|
|
}
|
|
|
|
return $base_dt;
|
|
}
|
|
|
|
sub days_between {
|
|
my $self = shift;
|
|
my $start_dt = shift;
|
|
my $end_dt = shift;
|
|
|
|
# Change time zone for date math and swap if needed
|
|
$start_dt = $start_dt->clone->set_time_zone('floating')->truncate( to => 'day' );
|
|
$end_dt = $end_dt->clone->set_time_zone('floating')->truncate( to => 'day' );
|
|
if ( $start_dt->compare($end_dt) > 0 ) {
|
|
|
|
( $start_dt, $end_dt ) = ( $end_dt, $start_dt );
|
|
}
|
|
|
|
# start and end should not be closed days
|
|
my $delta_days = $start_dt->delta_days($end_dt)->delta_days;
|
|
while ( $start_dt->compare($end_dt) < 1 ) {
|
|
$delta_days-- if $self->is_holiday($start_dt);
|
|
$start_dt->add( days => 1 );
|
|
}
|
|
return DateTime::Duration->new( days => $delta_days );
|
|
}
|
|
|
|
sub hours_between {
|
|
my ( $self, $start_date, $end_date ) = @_;
|
|
my $start_dt = $start_date->clone()->set_time_zone('floating');
|
|
my $end_dt = $end_date->clone()->set_time_zone('floating');
|
|
|
|
my $duration = $end_dt->delta_ms($start_dt);
|
|
$start_dt->truncate( to => 'day' );
|
|
$end_dt->truncate( to => 'day' );
|
|
|
|
# NB this is a kludge in that it assumes all days are 24 hours
|
|
# However for hourly loans the logic should be expanded to
|
|
# take into account open/close times then it would be a duration
|
|
# of library open hours
|
|
my $skipped_days = 0;
|
|
while ( $start_dt->compare($end_dt) < 1 ) {
|
|
$skipped_days++ if $self->is_holiday($start_dt);
|
|
$start_dt->add( days => 1 );
|
|
}
|
|
|
|
if ($skipped_days) {
|
|
$duration->subtract_duration( DateTime::Duration->new( hours => 24 * $skipped_days ) );
|
|
}
|
|
|
|
return $duration;
|
|
}
|
|
|
|
sub has_business_days_between {
|
|
my ( $self, $start_dt, $end_dt ) = @_;
|
|
|
|
# Clone dates to avoid modifying originals
|
|
my $current_date = $start_dt->clone->set_time_zone('floating');
|
|
my $end_date = $end_dt->clone->set_time_zone('floating');
|
|
|
|
# Ensure start is before end
|
|
if ( $current_date->compare($end_date) >= 0 ) {
|
|
return 0; # Same date or start after end
|
|
}
|
|
|
|
# Check each day between start and end (exclusive)
|
|
$current_date->add( days => 1 );
|
|
while ( $current_date->compare($end_date) < 0 ) {
|
|
return 1 unless $self->is_holiday($current_date);
|
|
$current_date->add( days => 1 );
|
|
}
|
|
|
|
return 0; # No business days found
|
|
}
|
|
|
|
sub set_daysmode {
|
|
my ( $self, $mode ) = @_;
|
|
|
|
# if not testing this is a no op
|
|
if ( $self->{test} ) {
|
|
$self->{days_mode} = $mode;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub clear_weekly_closed_days {
|
|
my $self = shift;
|
|
$self->{weekly_closed_days} = [ 0, 0, 0, 0, 0, 0, 0 ]; # Sunday only
|
|
return;
|
|
}
|
|
|
|
sub add_weekly_closure {
|
|
my ( $self, $params ) = @_;
|
|
return Koha::Library::Calendar::WeeklyClosure->new( { library_id => $self->{branchcode}, %$params } )->store;
|
|
}
|
|
|
|
sub add_repeating_closure {
|
|
my ( $self, $params ) = @_;
|
|
return Koha::Library::Calendar::RepeatingClosure->new( { library_id => $self->{branchcode}, %$params } )->store;
|
|
}
|
|
|
|
sub add_single_closure {
|
|
my ( $self, $params ) = @_;
|
|
return Koha::Library::Calendar::SingleClosure->new( { library_id => $self->{branchcode}, %$params } )->store;
|
|
}
|
|
|
|
sub add_exception {
|
|
my ( $self, $params ) = @_;
|
|
return Koha::Library::Calendar::Exception->new( { library_id => $self->{branchcode}, %$params } )->store;
|
|
}
|
|
|
|
sub delete_weekly_closure {
|
|
my ( $self, $params ) = @_;
|
|
my $row = Koha::Library::Calendar::WeeklyClosures->search(
|
|
{ library_id => $self->{branchcode}, weekday => $params->{weekday} },
|
|
{ rows => 1 }
|
|
)->single;
|
|
$row->delete if $row;
|
|
return $self;
|
|
}
|
|
|
|
sub delete_repeating_closure {
|
|
my ( $self, $params ) = @_;
|
|
my $row = Koha::Library::Calendar::RepeatingClosures->search(
|
|
{ library_id => $self->{branchcode}, day => $params->{day}, month => $params->{month} },
|
|
{ rows => 1 }
|
|
)->single;
|
|
$row->delete if $row;
|
|
return $self;
|
|
}
|
|
|
|
sub delete_single_closure {
|
|
my ( $self, $params ) = @_;
|
|
my $row = Koha::Library::Calendar::SingleClosures->search(
|
|
{ library_id => $self->{branchcode}, date => $params->{date} },
|
|
{ rows => 1 }
|
|
)->single;
|
|
$row->delete if $row;
|
|
return $self;
|
|
}
|
|
|
|
sub delete_exception {
|
|
my ( $self, $params ) = @_;
|
|
my $row = Koha::Library::Calendar::Exceptions->search(
|
|
{ library_id => $self->{branchcode}, date => $params->{date} },
|
|
{ rows => 1 }
|
|
)->single;
|
|
$row->delete if $row;
|
|
return $self;
|
|
}
|
|
|
|
sub closed_dates_in_range {
|
|
my ( $self, $from_dt, $to_dt ) = @_;
|
|
|
|
croak 'Missing from_dt in closed_dates_in_range' unless $from_dt;
|
|
croak 'Missing to_dt in closed_dates_in_range' unless $to_dt;
|
|
|
|
my $holidays = $self->_holidays;
|
|
|
|
my @closed;
|
|
my $current = $from_dt->clone;
|
|
while ( $current <= $to_dt ) {
|
|
my $ymd = $current->ymd('');
|
|
|
|
# Exception (open override) wins over any other closure rule.
|
|
if ( defined $holidays->{$ymd} && $holidays->{$ymd} == 0 ) {
|
|
$current->add( days => 1 );
|
|
next;
|
|
}
|
|
|
|
if ( ( defined $holidays->{$ymd} && $holidays->{$ymd} == 1 )
|
|
|| $self->{weekly_closed_days}->[ $current->day_of_week % 7 ]
|
|
|| $self->{day_month_closed_days}->{ $current->month }->{ $current->day } )
|
|
{
|
|
push @closed, $current->ymd;
|
|
}
|
|
|
|
$current->add( days => 1 );
|
|
}
|
|
|
|
return \@closed;
|
|
}
|
|
|
|
sub copy_to {
|
|
my ( $self, $target_branchcode ) = @_;
|
|
croak "No target_branchcode" unless $target_branchcode;
|
|
|
|
my $today = dt_from_string()->ymd;
|
|
my $schema = Koha::Database->new->schema;
|
|
|
|
$schema->txn_do(
|
|
sub {
|
|
my $weekly = Koha::Library::Calendar::WeeklyClosures->search( { library_id => $self->{branchcode} } );
|
|
while ( my $row = $weekly->next ) {
|
|
next
|
|
if Koha::Library::Calendar::WeeklyClosures->search(
|
|
{ library_id => $target_branchcode, weekday => $row->weekday } )->count;
|
|
Koha::Library::Calendar::WeeklyClosure->new(
|
|
{
|
|
library_id => $target_branchcode, weekday => $row->weekday,
|
|
title => $row->title, description => $row->description,
|
|
}
|
|
)->store;
|
|
}
|
|
|
|
my $repeating = Koha::Library::Calendar::RepeatingClosures->search( { library_id => $self->{branchcode} } );
|
|
while ( my $row = $repeating->next ) {
|
|
next
|
|
if Koha::Library::Calendar::RepeatingClosures->search(
|
|
{ library_id => $target_branchcode, day => $row->day, month => $row->month } )->count;
|
|
Koha::Library::Calendar::RepeatingClosure->new(
|
|
{
|
|
library_id => $target_branchcode, day => $row->day, month => $row->month,
|
|
title => $row->title, description => $row->description,
|
|
}
|
|
)->store;
|
|
}
|
|
|
|
my $singles = Koha::Library::Calendar::SingleClosures->search(
|
|
{ library_id => $self->{branchcode}, date => { '>=' => $today } } );
|
|
while ( my $row = $singles->next ) {
|
|
next
|
|
if Koha::Library::Calendar::SingleClosures->search(
|
|
{ library_id => $target_branchcode, date => $row->date } )->count;
|
|
Koha::Library::Calendar::SingleClosure->new(
|
|
{
|
|
library_id => $target_branchcode, date => $row->date,
|
|
title => $row->title, description => $row->description,
|
|
}
|
|
)->store;
|
|
}
|
|
|
|
my $exceptions =
|
|
Koha::Library::Calendar::Exceptions->search( { library_id => $self->{branchcode}, date => { '>=' => $today } } );
|
|
while ( my $row = $exceptions->next ) {
|
|
next
|
|
if Koha::Library::Calendar::Exceptions->search( { library_id => $target_branchcode, date => $row->date } )
|
|
->count;
|
|
Koha::Library::Calendar::Exception->new(
|
|
{
|
|
library_id => $target_branchcode, date => $row->date,
|
|
title => $row->title, description => $row->description,
|
|
}
|
|
)->store;
|
|
}
|
|
}
|
|
);
|
|
|
|
# Per-row store() in SingleClosure/Exception flushes the target's
|
|
# _holidays cache on insert; flush defensively here in case copy_to
|
|
# added no new single/exception rows but the caller expects a clean
|
|
# cache state for the target.
|
|
Koha::Caches->get_instance()->clear_from_cache( $target_branchcode . '_holidays' );
|
|
|
|
return 1;
|
|
}
|
|
|
|
1;
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
Koha::Library::Calendar - Object containing a library's calendar
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
use Koha::Library::Calendar
|
|
|
|
my $c = Koha::Library::Calendar->new( branchcode => 'MAIN' );
|
|
my $dt = dt_from_string();
|
|
|
|
# are we open
|
|
$open = $c->is_holiday($dt);
|
|
# when will item be due if loan period = $dur (a DateTime::Duration object)
|
|
$duedate = $c->addDuration($dt,$dur,'days');
|
|
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
Implements those features of C4::Calendar needed for Staffs Rolling Loans
|
|
|
|
=head1 METHODS
|
|
|
|
=head2 _init
|
|
|
|
$calendar->_init();
|
|
|
|
Populates the object with repeatable closed days (weekly and day/month) for the
|
|
branch. Called automatically by C<new>.
|
|
|
|
=head2 _holidays
|
|
|
|
my $holidays = $calendar->_holidays();
|
|
|
|
Returns a hashref of special (one-off) holidays for the branch, keyed by
|
|
C<YYYYMMDD> date strings. Results are cached.
|
|
|
|
=head2 new : Create a calendar object
|
|
|
|
my $calendar = Koha::Library::Calendar->new( branchcode => 'MAIN' );
|
|
|
|
The option branchcode is required
|
|
|
|
|
|
=head2 addDuration
|
|
|
|
my $dt = $calendar->addDuration($date, $dur, $unit)
|
|
|
|
C<$date> is a DateTime object representing the starting date of the interval.
|
|
|
|
C<$offset> is a DateTime::Duration to add to it
|
|
|
|
C<$unit> is a string value 'days' or 'hours' toflag granularity of duration
|
|
|
|
Currently unit is only used to invoke Staffs return Monday at 10 am rule this
|
|
parameter will be removed when issuingrules properly cope with that
|
|
|
|
|
|
=head2 addHours
|
|
|
|
my $dt = $calendar->addHours($date, $dur, $return_by_hour )
|
|
|
|
C<$date> is a DateTime object representing the starting date of the interval.
|
|
|
|
C<$offset> is a DateTime::Duration to add to it
|
|
|
|
C<$return_by_hour> is an integer value representing the opening hour for the branch
|
|
|
|
=head2 get_push_amt
|
|
|
|
my $amt = $calendar->get_push_amt($date)
|
|
|
|
C<$date> is a DateTime object representing a closed return date
|
|
|
|
Using the days_mode syspref value and the nature of the closed return
|
|
date, return how many days we should jump forward to find another return date
|
|
|
|
=head2 addDays
|
|
|
|
my $dt = $calendar->addDays($date, $dur)
|
|
|
|
C<$date> is a DateTime object representing the starting date of the interval.
|
|
|
|
C<$offset> is a DateTime::Duration to add to it
|
|
|
|
C<$unit> is a string value 'days' or 'hours' toflag granularity of duration
|
|
|
|
Currently unit is only used to invoke Staffs return Monday at 10 am rule this
|
|
parameter will be removed when issuingrules properly cope with that
|
|
|
|
=head2 is_holiday
|
|
|
|
$yesno = $calendar->is_holiday($dt);
|
|
|
|
passed a DateTime object returns 1 if it is a closed day
|
|
0 if not according to the calendar
|
|
|
|
=head2 days_between
|
|
|
|
$duration = $calendar->days_between($start_dt, $end_dt);
|
|
|
|
Passed two dates returns a DateTime::Duration object measuring the length between them
|
|
ignoring closed days. Always returns a positive number irrespective of the
|
|
relative order of the parameters.
|
|
|
|
Note: This routine assumes neither the passed start_dt nor end_dt can be a closed day
|
|
|
|
=head2 hours_between
|
|
|
|
$duration = $calendar->hours_between($start_dt, $end_dt);
|
|
|
|
Passed two dates returns a DateTime::Duration object measuring the length between them
|
|
ignoring closed days. Always returns a positive number irrespective of the
|
|
relative order of the parameters.
|
|
|
|
Note: This routine assumes neither the passed start_dt nor end_dt can be a closed day
|
|
|
|
=head2 has_business_days_between
|
|
|
|
$boolean = $calendar->has_business_days_between($start_dt, $end_dt);
|
|
|
|
Passed two DateTime objects, returns 1 if there are any business days (non-holidays)
|
|
between the start and end dates (exclusive), 0 otherwise.
|
|
|
|
This method is useful for determining if business days were missed between two dates,
|
|
such as when checking if server downtime caused business days to be skipped.
|
|
|
|
Examples:
|
|
- Monday to Wednesday with Tuesday as business day: returns 1
|
|
- Monday to Tuesday (consecutive days): returns 0
|
|
- Friday to Sunday with Saturday as holiday: returns 0
|
|
- Same date: returns 0
|
|
|
|
=head2 next_open_days
|
|
|
|
$datetime = $calendar->next_open_days($duedate_dt, $to_add)
|
|
|
|
Passed a Datetime and number of days, returns another Datetime representing
|
|
the next open day after adding the passed number of days. It is intended for
|
|
use to calculate the due date when useDaysMode syspref is set to either
|
|
'Datedue', 'Calendar' or 'Dayweek'.
|
|
|
|
=head2 prev_open_days
|
|
|
|
$datetime = $calendar->prev_open_days($duedate_dt, $to_sub)
|
|
|
|
Passed a Datetime and a number of days, returns another Datetime
|
|
representing the previous open day after subtracting the number of passed
|
|
days. It is intended for use to calculate the due date when useDaysMode
|
|
syspref is set to either 'Datedue', 'Calendar' or 'Dayweek'.
|
|
|
|
=head2 days_forward
|
|
|
|
$datetime = $calendar->days_forward($start_dt, $to_add)
|
|
|
|
Passed a Datetime and number of days, returns another Datetime representing
|
|
the next open day after adding the passed number of days. It is intended for
|
|
use to calculate the due date when useDaysMode syspref is set to either
|
|
'Datedue', 'Calendar' or 'Dayweek'.
|
|
|
|
=head2 set_daysmode
|
|
|
|
For testing only allows the calling script to change days mode
|
|
|
|
=head2 clear_weekly_closed_days
|
|
|
|
In test mode changes the testing set of closed days to a new set with
|
|
no closed days. TODO passing an array of closed days to this would
|
|
allow testing of more configurations
|
|
|
|
=head2 add_holiday
|
|
|
|
Passed a datetime object this will add it to the calendar's list of
|
|
closed days. This is for testing so that we can alter the Calendar object's
|
|
list of specified dates
|
|
|
|
=head2 CRUD methods
|
|
|
|
=head3 add_weekly_closure
|
|
|
|
$calendar->add_weekly_closure({ weekday => 0, title => 'Sundays', description => '' });
|
|
|
|
=head3 add_repeating_closure
|
|
|
|
$calendar->add_repeating_closure({ day => 25, month => 12, title => 'Christmas', description => '' });
|
|
|
|
=head3 add_single_closure
|
|
|
|
$calendar->add_single_closure({ date => '2026-06-15', title => 'Staff day', description => '' });
|
|
|
|
=head3 add_exception
|
|
|
|
$calendar->add_exception({ date => '2026-12-25', title => 'Special opening', description => '' });
|
|
|
|
=head3 delete_weekly_closure
|
|
|
|
$calendar->delete_weekly_closure({ weekday => 0 });
|
|
|
|
=head3 delete_repeating_closure
|
|
|
|
$calendar->delete_repeating_closure({ day => 25, month => 12 });
|
|
|
|
=head3 delete_single_closure
|
|
|
|
$calendar->delete_single_closure({ date => '2026-06-15' });
|
|
|
|
=head3 delete_exception
|
|
|
|
$calendar->delete_exception({ date => '2026-12-25' });
|
|
|
|
=head3 closed_dates_in_range
|
|
|
|
my $dates = $calendar->closed_dates_in_range($from_dt, $to_dt);
|
|
|
|
Returns an arrayref of ISO date strings (YYYY-MM-DD) representing every
|
|
closed day between C<$from_dt> and C<$to_dt> (inclusive), taking
|
|
weekly, annual-repeating, single and exception (open override)
|
|
closures into account. Exceptions win: a date listed as an exception
|
|
is never returned even if a weekly or repeating rule would otherwise
|
|
close it.
|
|
|
|
=head3 copy_to
|
|
|
|
$calendar->copy_to($target_branchcode);
|
|
|
|
Copies closure rules to another branch inside a single transaction.
|
|
|
|
Weekly and repeating closures are always copied. Single closures and
|
|
exceptions are only copied when their date is today or later (past
|
|
dates are filtered out by design).
|
|
|
|
Rows already present on the target (matched by (library_id, weekday),
|
|
(library_id, day, month) or (library_id, date)) are skipped so the
|
|
operation is idempotent.
|
|
|
|
Flushes the target library's C<_holidays> cache before returning.
|
|
|
|
=head2 Internal methods
|
|
|
|
=head3 _init
|
|
|
|
Loads weekly closed days and repeating annual closures from the database
|
|
into the object's internal data structures.
|
|
|
|
=head3 _holidays
|
|
|
|
Returns a hashref of single closures and exceptions keyed by YYYYMMDD.
|
|
Values are 1 for closures and 0 for exceptions (open overrides).
|
|
Results are cached in memcache.
|
|
|
|
=head1 DIAGNOSTICS
|
|
|
|
Will croak if not passed a branchcode in new
|
|
|
|
=head1 BUGS AND LIMITATIONS
|
|
|
|
This only contains a limited subset of the functionality in C4::Calendar
|
|
Only enough to support Staffs Rolling loans
|
|
|
|
=head1 AUTHOR
|
|
|
|
Colin Campbell colin.campbell@ptfs-europe.com
|
|
|
|
=head1 LICENSE AND COPYRIGHT
|
|
|
|
Copyright (c) 2011 PTFS-Europe Ltd All rights reserved
|
|
|
|
Koha is free software; you can redistribute it and/or modify it
|
|
under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Koha is distributed in the hope that it will be useful, but
|
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with Koha; if not, see <https://www.gnu.org/licenses>.
|