@@ -, +, @@ closed --- C4/Calendar.pm | 62 +++- C4/Circulation.pm | 37 ++- C4/Overdues.pm | 31 +- Koha/Calendar.pm | 84 +++-- installer/data/mysql/updatedatabase.pl | 34 ++ .../prog/en/modules/tools/holidays.tt | 143 ++++++++- misc/cronjobs/fines.pl | 17 +- t/Calendar1.t | 333 ++++++++++++++++++++ tools/holidays.pl | 66 +++- 9 files changed, 755 insertions(+), 52 deletions(-) create mode 100755 t/Calendar1.t --- a/C4/Calendar.pm +++ a/C4/Calendar.pm @@ -17,13 +17,19 @@ package C4::Calendar; use strict; use warnings; -use vars qw($VERSION @EXPORT); +use vars qw($VERSION @ISA @EXPORT_OK @EXPORT); use Carp; use Date::Calc qw( Date_to_Days Today); use C4::Context; +BEGIN { + $VERSION = 3.07.00.049; + @ISA = qw(Exporter); + @EXPORT_OK = qw(setOpeningHours getOpeningHours); +} + use constant ISO_DATE_FORMAT => "%04d-%02d-%02d"; =head1 NAME @@ -722,6 +728,60 @@ sub daysBetween { return($count); } +sub setOpeningHours { + my ($branchcode, $weekcode, $openhour, $closehour) = @_; + + my $dbh = C4::Context->dbh; + my $sth = $dbh->prepare("SELECT * FROM openinghours WHERE branchcode = ? AND weekcode = ?"); + $sth->execute($branchcode, $weekcode); + + if ( not $sth->fetchrow_hashref ) { + my $sth2 = $dbh->prepare('INSERT INTO openinghours (branchcode, weekcode, openhour, closehour) VALUES(?,?,?,?)'); + + $sth2->execute( $branchcode, $weekcode, $openhour, $closehour ); + } + else { + my $sth3 = $dbh->prepare('UPDATE openinghours set openhour = ?, closehour = ? WHERE branchcode = ? AND weekcode = ? '); + + $sth3->execute( $openhour, $closehour, $branchcode, $weekcode ); + } +} + +sub getOpeningHours { + my ($branchcode) = @_; + my $dbh = C4::Context->dbh; + my $query = "SELECT * FROM openinghours "; + $query .= "WHERE branchcode = ?" if(defined $branchcode); + my $sth = $dbh->prepare($query); + + my %results; + + if($branchcode){ + $sth->execute($branchcode); + while ( my $hours = $sth->fetchrow_hashref ) { + $results{ $hours->{'weekcode'} } = $hours; + } + } + else{ + $sth->execute(); + my @tmp; + while ( my $row = $sth->fetchrow_hashref ) { + #add a few precalculated fields + my $t1 = DateTime::Format::DateParse->parse_datetime( $row->{openhour}); + my $t2 = DateTime::Format::DateParse->parse_datetime( $row->{closehour}); + $row->{hashopen} = {hour => $t1->hour(), minute => $t1->minute(), second => $t1->second()}; + $row->{hashclose} = {hour => $t2->hour(), minute => $t2->minute(), second => $t2->second() }; + $row->{totalminutes} = $t1->delta_ms($t2)->delta_minutes(); + + #build the hash + $results{ $row->{'branchcode'} } ||= {}; + $results{ $row->{'branchcode'} }->{$row->{'weekcode'}} = $row; + } + } + + return \%results; +} + 1; __END__ --- a/C4/Circulation.pm +++ a/C4/Circulation.pm @@ -33,6 +33,7 @@ use C4::Dates qw(format_date); use C4::Accounts; use C4::ItemCirculationAlertPreference; use C4::Message; +use C4::Calendar qw(getOpeningHours); use C4::Debug; use C4::Branch; # GetBranches use C4::Log; # logaction @@ -91,6 +92,7 @@ BEGIN { &AnonymiseIssueHistory &CheckIfIssuedToPatron &IsItemIssued + &CalcDateDue ); # subs to deal with returns @@ -120,6 +122,7 @@ BEGIN { &DeleteOfflineOperation &ProcessOfflineOperation ); + } =head1 NAME @@ -1729,7 +1732,7 @@ sub AddReturn { return (0, { BadBarcode => $barcode }); # no barcode means no item or borrower. bail out. } my $issue = GetItemIssue($itemnumber); -# warn Dumper($iteminformation); + if ($issue and $issue->{borrowernumber}) { $borrower = C4::Members::GetMemberDetails($issue->{borrowernumber}) or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existant borrowernumber '$issue->{borrowernumber}'\n" @@ -3228,6 +3231,24 @@ C<$borrower> = Borrower object C<$isrenewal> = Boolean: is true if we want to calculate the date due for a renewal. Else is false. =cut +sub CapDateDue{ + my ( $calcDueDate, $loanlength, $branch) = @_; + + # for hourly loans, we will limit the due date to close time + return $calcDueDate unless ($loanlength->{lengthunit} eq 'hours'); + +# my $now = DateTime->now( time_zone => C4::Context->tz() ); + my $branchHours = getOpeningHours(); + + my $close = $calcDueDate->clone()->truncate( to => 'days'); + $close->set($branchHours->{$branch}->{$calcDueDate->local_day_of_week()-1}->{hashclose}); + + if (DateTime->compare($calcDueDate, $close) > 0){ # we want the document back at close time + return $close; + } + + return $calcDueDate; +} sub CalcDateDue { my ( $startdate, $itemtype, $branch, $borrower, $isrenewal ) = @_; @@ -3255,13 +3276,17 @@ sub CalcDateDue { ->truncate( to => 'minute' ); } - + my $calendar = Koha::Calendar->new( branchcode => $branch ); # calculate the datedue as normal if ( C4::Context->preference('useDaysMode') eq 'Days' ) { # ignoring calendar if ( $loanlength->{lengthunit} eq 'hours' ) { - $datedue->add( hours => $loanlength->{$length_key} ); - } else { # days + if($loanlength->{issuelength} == 24){ + $datedue = $calendar->addDate( $datedue, 1); + } else { + $datedue->add( hours => $loanlength->{issuelength} ); + } + }else { # days $datedue->add( days => $loanlength->{$length_key} ); $datedue->set_hour(23); $datedue->set_minute(59); @@ -3274,7 +3299,6 @@ sub CalcDateDue { else { # days $dur = DateTime::Duration->new( days => $loanlength->{$length_key}); } - my $calendar = Koha::Calendar->new( branchcode => $branch ); $datedue = $calendar->addDate( $datedue, $dur, $loanlength->{lengthunit} ); if ($loanlength->{lengthunit} eq 'days') { $datedue->set_hour(23); @@ -3311,7 +3335,7 @@ sub CalcDateDue { } } - return $datedue; + return CapDateDue($datedue, $loanlength, $branch); } @@ -3761,4 +3785,3 @@ __END__ Koha Development Team =cut - --- a/C4/Overdues.pm +++ a/C4/Overdues.pm @@ -41,6 +41,7 @@ BEGIN { # subs to rename (and maybe merge some...) push @EXPORT, qw( &CalcFine + &_get_chargeable_units &Getoverdues &checkoverdues &NumberNotifyId @@ -110,8 +111,9 @@ sub Getoverdues { if ( C4::Context->preference('item-level_itypes') ) { $statement = " SELECT issues.*, items.itype as itemtype, items.homebranch, items.barcode - FROM issues + FROM issues LEFT JOIN items USING (itemnumber) +join issuingrules on issuingrules.itemtype = items.itype WHERE date_due < NOW() "; } else { @@ -135,6 +137,11 @@ LEFT JOIN biblioitems USING (biblioitemnumber) $statement .= ' AND ( TO_DAYS( NOW() )-TO_DAYS( date_due ) ) < ? '; push @bind_parameters, $params->{'maximumdays'}; } + + if($params->{'shortrun'}){ + $statement .= " and issuingrules.lengthunit in ('hours') " ; + } + $statement .= 'ORDER BY borrowernumber'; my $sth = $dbh->prepare( $statement ); $sth->execute( @bind_parameters ); @@ -240,7 +247,7 @@ or "Final Notice". But CalcFine never defined any value. =cut sub CalcFine { - my ( $item, $bortype, $branchcode, $due_dt, $end_date ) = @_; + my ( $item, $bortype, $branchcode, $due_dt, $end_date, $openingHours ) = @_; my $start_date = $due_dt->clone(); # get issuingrules (fines part will be used) my $itemtype = $item->{itemtype} || $item->{itype}; @@ -248,7 +255,7 @@ sub CalcFine { my $fine_unit = $data->{lengthunit}; $fine_unit ||= 'days'; - my $chargeable_units = _get_chargeable_units($fine_unit, $start_date, $end_date, $branchcode); + my $chargeable_units = _get_chargeable_units($fine_unit, $start_date, $end_date, $branchcode, $openingHours); my $units_minus_grace = $chargeable_units - $data->{firstremind}; my $amount = 0; if ($data->{'chargeperiod'} && ($units_minus_grace > 0) ) { @@ -282,25 +289,29 @@ C<$branchcode> is the branch whose calendar to use for finding holidays. =cut sub _get_chargeable_units { - my ($unit, $dt1, $dt2, $branchcode) = @_; + my ($unit, $dt1, $dt2, $branchcode, $openingHours) = @_; my $charge_units = 0; my $charge_duration; if ($unit eq 'hours') { if(C4::Context->preference('finesCalendar') eq 'noFinesWhenClosed') { - my $calendar = Koha::Calendar->new( branchcode => $branchcode ); - $charge_duration = $calendar->hours_between( $dt1, $dt2 ); + if (!defined $openingHours->{$branchcode}{'Calendar'}){ + $openingHours->{$branchcode}{'Calendar'} = Koha::Calendar->new( branchcode => $branchcode ); + } + $charge_duration = $openingHours->{$branchcode}{'Calendar'}->hours_between( $dt1, $dt2, $openingHours->{$branchcode} ); } else { $charge_duration = $dt2->delta_ms( $dt1 ); } - if($charge_duration->in_units('hours') == 0 && $charge_duration->in_units('seconds') > 0){ + if($charge_duration->in_units($unit) == 0 && $charge_duration->in_units('seconds') > 0){ return 1; } - return $charge_duration->in_units('hours'); + return $charge_duration->in_units($unit); } else { # days if(C4::Context->preference('finesCalendar') eq 'noFinesWhenClosed') { - my $calendar = Koha::Calendar->new( branchcode => $branchcode ); - $charge_duration = $calendar->days_between( $dt1, $dt2 ); + if (!defined $openingHours->{$branchcode}{'Calendar'}){ + $openingHours->{$branchcode}{'Calendar'} = Koha::Calendar->new( branchcode => $branchcode ); + } + $charge_duration = $openingHours->{$branchcode}{'Calendar'}->days_between( $dt1, $dt2, $openingHours->{$branchcode} ); } else { $charge_duration = $dt2->delta_days( $dt1 ); } --- a/Koha/Calendar.pm +++ a/Koha/Calendar.pm @@ -271,7 +271,7 @@ sub days_between { my $self = shift; my $start_dt = shift; my $end_dt = shift; - + my $branchHours = shift; if ( $start_dt->compare($end_dt) > 0 ) { # swap dates my $int_dt = $end_dt; @@ -279,47 +279,79 @@ sub days_between { $start_dt = $int_dt; } - - # start and end should not be closed days my $days = $start_dt->delta_days($end_dt)->delta_days; - for (my $dt = $start_dt->clone(); - $dt <= $end_dt; - $dt->add(days => 1) - ) { - if ($self->is_holiday($dt)) { - $days--; + # start and end should not be closed days + while ($start_dt <= $end_dt) { + if (defined $branchHours->{holidays}{$start_dt->ymd()}) { + $days-- if $branchHours->{holidays}{$start_dt->ymd()}; + } else { + $branchHours->{holidays}{$start_dt->ymd()} = $self->is_holiday($start_dt); + $days-- if $branchHours->{holidays}{$start_dt->ymd()}; } + $start_dt->add(days => 1); } return DateTime::Duration->new( days => $days ); } sub hours_between { - my ($self, $start_date, $end_date) = @_; + my ($self, $start_date, $end_date, $branchHours) = @_; + my $start_dt = $start_date->clone(); my $end_dt = $end_date->clone(); - 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; - for (my $dt = $start_dt->clone(); - $dt <= $end_dt; - $dt->add(days => 1) - ) { - if ($self->is_holiday($dt)) { - ++$skipped_days; + + if (defined $branchHours->{0}){ # We were given the branch's opening hours. + my $totalMinutes = 0; + for (my $dt = $start_dt->clone(); $dt <= $end_dt; $dt->add(days => 1)) { + # hash to keep track of holidays + if (defined $branchHours->{holidays}{$dt->ymd()}) { + next if $branchHours->{holidays}{$dt->ymd()}; + } else { + $branchHours->{holidays}{$dt->ymd()} = $self->is_holiday($dt); + next if ($self->is_holiday($dt)); + } + + if(DateTime->compare($dt,$start_dt) == 0){ # from location till close time + #create a timestamp for today's close time using the current day object with the branchHours's hours. + my $close = $dt->clone()->set($branchHours->{$dt->local_day_of_week()-1}->{hashclose}); + + if(DateTime->compare($end_date, $close) < 0){ # we are still the same day, before closing time + $totalMinutes += $start_date->delta_ms($end_date)->delta_minutes(); + } elsif(DateTime->compare($start_date, $close) < 0) { # we started (issue) before closing hour + $totalMinutes += $start_date->delta_ms($close)->delta_minutes(); + } + } + elsif(DateTime->compare($dt, $end_dt) == 0){ # from open time till now + my $open = $dt->clone()->set($branchHours->{$dt->local_day_of_week()-1}->{hashopen}); + if(DateTime->compare($end_date, $open) > 0) { # we end (return issue) after opening hour + $totalMinutes += $end_date->delta_ms($open)->delta_minutes(); + } + } + else{ + $totalMinutes += $branchHours->{$dt->local_day_of_week()- 1}->{totalminutes}; + } } + + return DateTime::Duration->new( minutes => $totalMinutes); } - if ($skipped_days) { - $duration->subtract_duration(DateTime::Duration->new( hours => 24 * $skipped_days)); + else{ + my $duration = $end_dt->delta_ms($start_dt); + my $skipped_days = 0; + while ($start_dt <= $end_dt){ + if ($self->is_holiday($start_dt)) { + ++$skipped_days; + } + $start_dt->add(days => 1); + } + if ($skipped_days) { + $duration->subtract_duration(DateTime::Duration->new( hours => 24 * $skipped_days)); + } + return $duration; } - return $duration; - } sub set_daysmode { --- a/installer/data/mysql/updatedatabase.pl +++ a/installer/data/mysql/updatedatabase.pl @@ -8560,6 +8560,40 @@ if ( CheckVersion($DBversion) ) { SetVersion($DBversion); } +$DBversion ="XXX"; +if( CheckVersion($DBversion) ){ + $dbh->do(q{ + CREATE TABLE IF NOT EXISTS openinghours ( + branchcode varchar(10) NOT NULL, + weekcode int(2) NOT NULL, + openhour time NOT NULL, + closehour time NOT NULL, + PRIMARY KEY (branchcode,weekcode) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + }); + $dbh->do(q{ + ALTER TABLE openinghours + ADD CONSTRAINT openinghours_ibfk_1 + FOREIGN KEY (branchcode) REFERENCES + branches(branchcode); + }); + + my $sth = $dbh->prepare("SELECT branchcode FROM branches"); + $sth->execute; + my @branches; + my $weekcode; + while (@branches = $sth->fetchrow_array()){ + for ($weekcode = 0; $weekcode <= 6; $weekcode++) { + my $count = $dbh->selectrow_array('SELECT count(*) FROM openinghours WHERE branchcode=? and weekcode=?',undef, $branches[0], $weekcode); + if ($count==0){ + $dbh->do('INSERT INTO openinghours (branchcode, weekcode, openhour, closehour) VALUES (?, ?, ? ,?)', undef, $branches[0], $weekcode, '09:00:00', '17:00:00') or die $dbh->errstr; + } + } + } + print "Calendar opening hours set.\n"; + SetVersion($DBversion); +} + =head1 FUNCTIONS =head2 TableExists($table) --- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/holidays.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/holidays.tt @@ -6,6 +6,10 @@ [% INCLUDE 'datatables.inc' %]