From 550940a90011b79f74a27370cd20241b826d6b1d 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 'ConsiderLibraryHoursInCirculation' 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 ConsiderLibraryHoursInCirculation. 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 ConsiderLibraryHoursInCirculation 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 ConsiderLibraryHoursInCirculation 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 Signed-off-by: Sam Lau Signed-off-by: Martin Renvoize --- C4/Circulation.pm | 66 +++++++++++++++++- t/db_dependent/Circulation/CalcDateDue.t | 85 +++++++++++++++++++++++- 2 files changed, 145 insertions(+), 6 deletions(-) diff --git a/C4/Circulation.pm b/C4/Circulation.pm index 109809d4b99..f7980f08b2c 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 ); @@ -3891,11 +3892,44 @@ sub CalcDateDue { } ); + my $considerlibraryhours = C4::Context->preference('ConsiderLibraryHoursInCirculation'); + + # 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({ library_id => $branch, day => $dayofweek }); + my @close = undef; + my $tomorrowhours = Koha::Library::Hours->find({ library_id => $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); @@ -3903,17 +3937,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 215e2e7e884..96e1f3a2106 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::Hours; use Koha::CirculationRules; use_ok('C4::Circulation', qw( CalcDateDue )); @@ -18,7 +19,7 @@ my $schema = Koha::Database->new->schema; $schema->storage->txn_begin; my $builder = t::lib::TestBuilder->new; - +t::lib::Mocks::mock_preference( 'ConsiderLibraryHoursInCirculation', 'ignore' ); my $library = $builder->build_object({ class => 'Koha::Libraries' })->store; my $dateexpiry = '2013-01-01'; my $patron_category = $builder->build_object({ class => 'Koha::Patron::Categories', value => { category_type => 'B' } })->store; @@ -356,5 +357,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 => $_, library_id => $library1->{branchcode}, open_time => $open, close_time => $close })->store; +} + +# ignore calendar +t::lib::Mocks::mock_preference('useDaysMode', 'Days'); +t::lib::Mocks::mock_preference('ConsiderLibraryHoursInCirculation', '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 ConsiderLibraryHoursInCirculation 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 ConsiderLibraryHoursInCirculation 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('ConsiderLibraryHoursInCirculation', '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 ConsiderLibraryHoursInCirculation is set to close time" ); + +t::lib::Mocks::mock_preference( 'ConsiderLibraryHoursInCirculation', '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 ConsiderLibraryHoursInCirculation is set to open time" ); + $cache->clear_from_cache($key); $schema->storage->txn_rollback; -- 2.43.0