From f34d1c2999de260ab363519f58763e9fa9441053 Mon Sep 17 00:00:00 2001 From: Aleisha Amohia Date: Wed, 8 Sep 2021 03:59:12 +1200 Subject: [PATCH] Bug 6796: Consider library hours when calculating due date + tests This feature adds the ability to set opening and closing hours for your library and for these hours to be considered when calculating due dates for hourly loans. If the due date for an hourly loan falls after the library closes, the library can choose for the due date to be shortened to meet the close time, or extended to meet the open time the next day. This feature adds a new table 'branch_hours' for storing the open and close times per day for each library, and a new system preference 'ConsiderLibraryHoursWhenIssuing' to choose which behaviour should be followed when calculating due dates. To test: 1. Apply patches and update database. Upgrade schema if not applying patch with schema changes. Restart services. 2. Go to Administration -> Libraries. Edit a library and scroll to the bottom to find the 'opening hours' section. Test adding and removing open and close times on various days. Confirm saving works as expected. 3. Add a new library and test adding open and close times works as expected. 4. Edit your default library and save open and close times for each day. 5. Go to Administration -> Circulation and fine rules. Edit a rule, set the unit to 'hours' and set the loan period to a number that would cause a checkout to be due after the close time you just set, i.e. if you set your close time to be 5pm and your system time is currently 1pm, set the loan period to be 5 (5 hours) so the calculated due date would be 6pm. 6. Go to Administration -> system preferences. Search for ConsiderLibraryHoursWhenIssuing. It should be under 'Checkout policy' in the Circulation system preferences. Confirm the pre-selected option is 'ignore'. Keep this tab open. 6. In a new tab, get the barcode for an item that has an itemtype matching the circulation rule you just set. 7. Go to the checkouts for a patron that has a categorycode matching the circulation rule you just set. 8. Check out your item. Confirm that the checkout is due at the end of the loan period, not taking closing hours into consideration. Return the item. 9. Back in your other tab, set ConsiderLibraryHoursWhenIssuing to 'close', so the due date should be shortened to meet the close time. 10. Check out your item. Confirm the checkout is due when the library closes. Return the item. 11. Back in your other tab, set ConsiderLibraryHoursWhenIssuing to 'open', so the due date should be extended to meet the opening time. 12. Check out your item. Confirm the checkout is due the next day when the library opens. 13. Confirm tests pass t/db_dependent/Circulation/CalcDateDue.t Sponsored-by: Catalyst IT --- C4/Circulation.pm | 65 ++++++++++++++++++- t/db_dependent/Circulation/CalcDateDue.t | 83 +++++++++++++++++++++++- 2 files changed, 143 insertions(+), 5 deletions(-) diff --git a/C4/Circulation.pm b/C4/Circulation.pm index 0f1b6914aa6..92351b75d4c 100644 --- a/C4/Circulation.pm +++ b/C4/Circulation.pm @@ -63,6 +63,7 @@ use Koha::SearchEngine::Indexer; use Koha::Exceptions::Checkout; use Koha::Plugins; use Koha::Recalls; +use Koha::Library::Hours; use Carp qw( carp ); use List::MoreUtils qw( any ); use Scalar::Util qw( looks_like_number blessed ); @@ -3821,11 +3822,43 @@ sub CalcDateDue { } ); + my $considerlibraryhours = C4::Context->preference('ConsiderLibraryHoursWhenIssuing'); + # starter vars so don't do calculations directly to $datedue + my $potential_datedue = $datedue->clone; + my $library_close = $datedue->clone; + my $dayofweek = $datedue->local_day_of_week - 1; + my $todayhours = Koha::Library::Hours->find({ branchcode => $branch, day => $dayofweek }); + my @close = undef; + my $tomorrowhours = Koha::Library::Hours->find({ branchcode => $branch, day => $dayofweek+1 }); # get open hours of next day + my @open = undef; + if ( $todayhours->close_time and $tomorrowhours->open_time ) { + @close = split( ":", $todayhours->close_time ); + $library_close = $library_close->set( hour => $close[0], minute => $close[1] ); + $potential_datedue = $potential_datedue->add( hours => $loanlength->{$length_key} ); # datedue without consideration for open hours + @open = split( ":", $tomorrowhours->open_time ); + } + # calculate the datedue as normal if ( $daysmode eq 'Days' ) { # ignoring calendar if ( $loanlength->{lengthunit} eq 'hours' ) { - $datedue->add( hours => $loanlength->{$length_key} ); + if ( $potential_datedue > $library_close and $todayhours->close_time and $tomorrowhours->open_time ) { + if ( $considerlibraryhours eq 'close' ) { + # datedue will be after the library closes on that day + # shorten loan period to end when library closes + $datedue->set( hour => $close[0], minute => $close[1] ); + } elsif ( $considerlibraryhours eq 'open' ) { + # datedue will be after the library closes on that day + # extend loan period to when library opens following day + $datedue->add( days => 1 )->set( hour => $open[0], minute => $open[1] ); + } else { + # ignore library open hours + $datedue->add( hours => $loanlength->{$length_key} ); + } + } else { + # due time doesn't conflict with library open hours, don't need to check + $datedue->add( hours => $loanlength->{$length_key} ); + } } else { # days $datedue->add( days => $loanlength->{$length_key} ); $datedue->set_hour(23); @@ -3833,17 +3866,43 @@ sub CalcDateDue { } } else { my $dur; + my $sethours; if ($loanlength->{lengthunit} eq 'hours') { - $dur = DateTime::Duration->new( hours => $loanlength->{$length_key}); + if ( $potential_datedue > $library_close and $todayhours->close_time and $tomorrowhours->open_time ) { + if ( $considerlibraryhours eq 'close' ) { + # datedue will be after the library closes on that day + # shorten loan period to end when library closes + $dur = $potential_datedue->delta_ms( $library_close ); + $sethours = $considerlibraryhours; + } elsif ( $considerlibraryhours eq 'open' ) { + # datedue will be after the library closes on that day + # extend loan period to when library opens following day + my $library_open = $datedue->clone->set( hour => $open[0], minute => $open[1] ); + $dur = $potential_datedue->delta_ms( $library_open )->add( days => 1 ); + $sethours = $considerlibraryhours; + } else { + # ignore library open hours + $dur = DateTime::Duration->new( hours => $loanlength->{$length_key} ); + } + } else { + # due time doesn't conflict with library open hours, don't need to check + $dur = DateTime::Duration->new( hours => $loanlength->{$length_key} ); + } } else { # days - $dur = DateTime::Duration->new( days => $loanlength->{$length_key}); + $dur = DateTime::Duration->new( days => $loanlength->{$length_key} ); } my $calendar = Koha::Calendar->new( branchcode => $branch, days_mode => $daysmode ); $datedue = $calendar->addDuration( $datedue, $dur, $loanlength->{lengthunit} ); if ($loanlength->{lengthunit} eq 'days') { $datedue->set_hour(23); $datedue->set_minute(59); + } else { + if ( $sethours and $sethours eq 'close' ) { + $datedue->set( hour => $close[0], minute => $close[1] ); + } elsif ( $sethours and $sethours eq 'open' ) { + $datedue->set( hour => $open[0], minute => $open[1] ); + } } } diff --git a/t/db_dependent/Circulation/CalcDateDue.t b/t/db_dependent/Circulation/CalcDateDue.t index cdcd8008700..faa1f4af35b 100755 --- a/t/db_dependent/Circulation/CalcDateDue.t +++ b/t/db_dependent/Circulation/CalcDateDue.t @@ -2,14 +2,15 @@ use Modern::Perl; -use Test::More tests => 19; +use Test::More tests => 23; use Test::MockModule; use DBI; use DateTime; use t::lib::Mocks; use t::lib::TestBuilder; use C4::Calendar qw( new insert_single_holiday delete_holiday insert_week_day_holiday ); - +use Koha::DateUtils qw( dt_from_string ); +use Koha::Library::Hour; use Koha::CirculationRules; use_ok('C4::Circulation', qw( CalcDateDue )); @@ -340,5 +341,83 @@ my $renewed_date = $start_date->clone->add( days => 7 ); $date = C4::Circulation::CalcDateDue( $start_date, $itemtype, $branchcode, $borrower, 1 ); is( $date->ymd, $renewed_date->ymd, 'Renewal period of "" should trigger fallover to issuelength for renewal' ); +# Testing hourly loans consider library open hours + +my $library1 = $builder->build( { source => 'Branch' } ); +Koha::CirculationRules->set_rules( + { + categorycode => $categorycode, + itemtype => $itemtype, + branchcode => $library1->{branchcode}, + rules => { + issuelength => 3, # loan period is 3 hours + lengthunit => 'hours', + } + } +); + +my $open = DateTime->now->subtract( hours => 4 )->hms; +my $close = DateTime->now->add( hours => 2 )->hms; +my $now = DateTime->now; + +foreach (0..6) { + # library opened 4 hours ago and closes in 2 hours. + Koha::Library::Hour->new({ day => $_, branchcode => $library1->{branchcode}, open_time => $open, close_time => $close })->store; +} + +# ignore calendar +t::lib::Mocks::mock_preference('useDaysMode', 'Days'); +t::lib::Mocks::mock_preference('ConsiderLibraryHoursWhenIssuing', 'close'); +# shorten loan period + +$date = C4::Circulation::CalcDateDue( $now, $itemtype, $library1->{branchcode}, $borrower ); +my $expected_duetime = DateTime->now->add( hours => 2 ); +is( $date, $expected_duetime, "Loan period was shortened because ConsiderLibraryHoursWhenIssuing is set to close time" ); + +t::lib::Mocks::mock_preference('ConsiderLibraryHoursWhenIssuing', 'open'); +# extend loan period + +$date = C4::Circulation::CalcDateDue( $now, $itemtype, $library1->{branchcode}, $borrower ); +$expected_duetime = DateTime->now->add( days => 1 )->subtract( hours => 4 ); +is( $date, $expected_duetime, "Loan period was extended because ConsiderLibraryHoursWhenIssuing is set to open time" ); + +my $holiday_tomorrow = DateTime->now->add( days => 1 ); + +# consider calendar +my $library1_calendar = C4::Calendar->new( branchcode => $library1->{branchcode} ); +$library1_calendar->insert_single_holiday( + day => $holiday_tomorrow->day, + month => $holiday_tomorrow->month, + year => $holiday_tomorrow->year, + title => 'testholiday', + description => 'testholiday' +); +Koha::CirculationRules->set_rules( + { + categorycode => $categorycode, + itemtype => $itemtype, + branchcode => $library1->{branchcode}, + rules => { + issuelength => 13, # loan period must cross over into tomorrow + lengthunit => 'hours', + } + } +); + +t::lib::Mocks::mock_preference('useDaysMode', 'Calendar'); +t::lib::Mocks::mock_preference('ConsiderLibraryHoursWhenIssuing', 'close'); +# shorten loan period + +$date = C4::Circulation::CalcDateDue( $now, $itemtype, $library1->{branchcode}, $borrower ); +$expected_duetime = DateTime->now->add( days => 2, hours => 2 ); +is( $date, $expected_duetime, "Loan period was shortened (but considers the holiday) because ConsiderLibraryHoursWhenIssuing is set to close time" ); + +t::lib::Mocks::mock_preference('ConsiderLibraryHoursWhenIssuing', 'open'); +# extend loan period + +$date = C4::Circulation::CalcDateDue( $now, $itemtype, $library1->{branchcode}, $borrower ); +$expected_duetime = DateTime->now->add( days => 2 )->subtract( hours => 4 ); +is( $date, $expected_duetime, "Loan period was extended (but considers the holiday) because ConsiderLibraryHoursWhenIssuing is set to open time" ); + $cache->clear_from_cache($key); $schema->storage->txn_rollback; -- 2.30.2