From 3efc0b381d8b99873ed52ebf6389e42006dacb47 Mon Sep 17 00:00:00 2001 From: Kyle M Hall Date: Fri, 2 Feb 2018 08:36:02 -0500 Subject: [PATCH] Bug 14364: Allow automatically canceled expired waiting holds to fill the next hold Right now, if a library automatically cancels expired waiting holds, a librarian must still re-checkin an item to trap the next available hold for that item. It would be better if the next hold was automatically trapped and the librarians receive an email notification so they can make any changes to the item if need be ( hold area, hold slip in item, etc ). Test Plan: 1) Apply this patch 2) Run updatedatabase.pl 3) Create a record with one item 4) Place two holds on that record 5) Check in the item and set it to waiting for the first patron 6) Set ReservesMaxPickUpDelay to 1 7) Enable ExpireReservesMaxPickUpDelay 8) Enable ExpireReservesAutoFill 9) Set an email address in ExpireReservesAutoFillEmail 10) Modify the holds waitingdate to be in the past 11) Run misc/cronjobs/holds/cancel_expired_holds.pl 12) Note the hold is now waiting for the next patron 12) Note a waiting hold notification email was sent to that patron 13) Note a hold changed notification email was sent to the library Signed-off-by: Victoria Faafia --- C4/Reserves.pm | 40 +++- Koha/Hold.pm | 21 +- .../data/mysql/atomicupdate/bug_14364.perl | 22 ++ .../fr-FR/1-Obligatoire/sample_notices.sql | 9 + installer/data/mysql/mandatory/sysprefs.sql | 2 + .../admin/preferences/circulation.pref | 12 +- t/db_dependent/Holds/ExpireReservesAutoFill.t | 190 ++++++++++++++++++ 7 files changed, 290 insertions(+), 6 deletions(-) create mode 100644 installer/data/mysql/atomicupdate/bug_14364.perl create mode 100755 t/db_dependent/Holds/ExpireReservesAutoFill.t diff --git a/C4/Reserves.pm b/C4/Reserves.pm index 54af13d7f0..f58c406460 100644 --- a/C4/Reserves.pm +++ b/C4/Reserves.pm @@ -971,6 +971,7 @@ sub CancelExpiredReserves { if ( defined($hold->found) && $hold->found eq 'W' ) { $cancel_params->{charge_cancel_fee} = 1; } + $cancel_params->{autofill} = C4::Context->preference('ExpireReservesAutoFill'); $hold->cancel( $cancel_params ); } } @@ -1137,7 +1138,7 @@ sub ModReserveStatus { =head2 ModReserveAffect - &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id, $desk_id); + &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id, $desk_id, $notify_library); This function affect an item and a status for a given reserve, either fetched directly by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber @@ -1153,7 +1154,7 @@ This function also removes any entry of the hold in holds queue table. =cut sub ModReserveAffect { - my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id, $desk_id ) = @_; + my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id, $desk_id, $notify_library ) = @_; my $dbh = C4::Context->dbh; # we want to attach $itemnumber to $borrowernumber, find the biblionumber @@ -1187,7 +1188,7 @@ sub ModReserveAffect { $hold->set_processing(); } else { $hold->set_waiting($desk_id); - _koha_notify_reserve( $hold->reserve_id ) unless $already_on_shelf; + _koha_notify_reserve( $hold->reserve_id, $notify_library ) unless $already_on_shelf; # Complete transfer if one exists my $transfer = $hold->item->get_transfer; $transfer->receive if $transfer; @@ -1812,6 +1813,8 @@ The following tables are availalbe witin the notice: sub _koha_notify_reserve { my $reserve_id = shift; + my $notify_library = shift; + my $hold = Koha::Holds->find($reserve_id); my $borrowernumber = $hold->borrowernumber; @@ -1879,6 +1882,37 @@ sub _koha_notify_reserve { &$send_notification('print', 'HOLD'); } + if ($notify_library) { + my $letter = C4::Letters::GetPreparedLetter( + module => 'reserves', + letter_code => 'HOLD_CHANGED', + branchcode => $hold->branchcode, + substitute => { today => output_pref( dt_from_string ) }, + tables => { + 'branches' => $library, + 'borrowers' => $patron->unblessed, + 'biblio' => $hold->biblionumber, + 'biblioitems' => $hold->biblionumber, + 'reserves' => $hold->unblessed, + 'items' => $hold->itemnumber, + }, + ); + + my $email = + C4::Context->preference('ExpireReservesAutoFillEmail') + || $library->{branchemail} + || C4::Context->preference('KohaAdminEmailAddress'); + + C4::Letters::EnqueueLetter( + { + letter => $letter, + borrowernumber => $borrowernumber, + message_transport_type => 'email', + from_address => $email, + to_address => $email, + } + ); + } } =head2 _ShiftPriorityByDateAndPriority diff --git a/Koha/Hold.pm b/Koha/Hold.pm index d4fb0e4c52..1ec5c15e5e 100644 --- a/Koha/Hold.pm +++ b/Koha/Hold.pm @@ -22,9 +22,10 @@ use Modern::Perl; use List::MoreUtils qw( any ); -use C4::Context; +use C4::Context qw(preference); use C4::Letters qw( GetPreparedLetter EnqueueLetter ); use C4::Log qw( logaction ); +use C4::Reserves; use Koha::AuthorisedValues; use Koha::DateUtils qw( dt_from_string ); @@ -532,6 +533,12 @@ Cancel a hold: sub cancel { my ( $self, $params ) = @_; +<<<<<<< HEAD +======= + + my $autofill_next = $params->{autofill} && $self->itemnumber && $self->found && $self->found eq 'W'; + +>>>>>>> Bug 14364: Allow automatically canceled expired waiting holds to fill the next hold $self->_result->result_source->schema->txn_do( sub { my $patron = $self->patron; @@ -584,7 +591,6 @@ sub cancel { if $patron->privacy == 2; $self->SUPER::delete(); # Do not add a DELETE log - # now fix the priority on the others.... C4::Reserves::_FixPriority({ biblionumber => $self->biblionumber }); @@ -609,6 +615,17 @@ sub cancel { if C4::Context->preference('HoldsLog'); } ); + + if ($autofill_next) { + my ( undef, $next_hold ) = C4::Reserves::CheckReserves( $self->itemnumber ); + if ($next_hold) { + my $is_transfer = $self->branchcode ne $next_hold->{branchcode}; + + C4::Reserves::ModReserveAffect( $self->itemnumber, $self->borrowernumber, $is_transfer, $next_hold->{reserve_id}, $self->desk_id, $autofill_next ); + C4::Reserves::ModItemTransfer( $self->itemnumber, $self->branchcode, $next_hold->{branchcode}, "Reserve" ) if $is_transfer; + } + } + return $self; } diff --git a/installer/data/mysql/atomicupdate/bug_14364.perl b/installer/data/mysql/atomicupdate/bug_14364.perl new file mode 100644 index 0000000000..2b50da6913 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_14364.perl @@ -0,0 +1,22 @@ +$DBversion = 'XXX'; # will be replaced by the RM +if( CheckVersion( $DBversion ) ) { + $dbh->do(q{ + INSERT IGNORE INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `type` ) VALUES + ('ExpireReservesAutoFill','0',NULL,'Automatically fill the next hold with a automatically canceled expired waiting hold.','YesNo'), + ('ExpireReservesAutoFillEmail','', NULL,'If ExpireReservesAutoFill and an email is defined here, the email notification for the change in the hold will be sent to this address.','Free'); + }); + + $dbh->do(q{ + INSERT IGNORE INTO letter(module,code,branchcode,name,is_html,title,content,message_transport_type) + VALUES ( 'reserves', 'HOLD_CHANGED', '', 'Canceled Hold Available for Different Patron', '0', 'Canceled Hold Available for Different Patron', 'The patron picking up <> (<>) has changed to <> <> (<>). + +Please update the hold information for this item. + +Title: <> +Author: <> +Copy: <> +Pickup location: <>', 'email'); + }); + + NewVersion( $DBversion, 14364, "Allow automatically canceled expired waiting holds to fill the next hold"); +} diff --git a/installer/data/mysql/fr-FR/1-Obligatoire/sample_notices.sql b/installer/data/mysql/fr-FR/1-Obligatoire/sample_notices.sql index e5686f3559..34aa841cd5 100644 --- a/installer/data/mysql/fr-FR/1-Obligatoire/sample_notices.sql +++ b/installer/data/mysql/fr-FR/1-Obligatoire/sample_notices.sql @@ -503,3 +503,12 @@ INSERT IGNORE INTO letter (module, code, name, title, content, message_transport [% END %] [% END %] ", 'email'); +INSERT INTO letter(module,code,branchcode,name,is_html,title,content,message_transport_type) + VALUES ( 'reserves', 'HOLD_CHANGED', '', 'Canceled Hold Available for Different Patron', '0', 'Canceled Hold Available for Different Patron', 'The patron picking up <> (<>) has changed to <> <> (<>). + +Please update the hold information for this item. + +Title: <> +Author: <> +Copy: <> +Pickup location: <>', 'email'); diff --git a/installer/data/mysql/mandatory/sysprefs.sql b/installer/data/mysql/mandatory/sysprefs.sql index deaf6b6fc2..f252d77ced 100644 --- a/installer/data/mysql/mandatory/sysprefs.sql +++ b/installer/data/mysql/mandatory/sysprefs.sql @@ -203,6 +203,8 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('EnhancedMessagingPreferences','1','','If ON, allows patrons to select to receive additional messages about items due or nearly due.','YesNo'), ('EnhancedMessagingPreferencesOPAC', '1', NULL, 'If ON, show patrons messaging setting on the OPAC.', 'YesNo'), ('expandedSearchOption','0',NULL,'If ON, set advanced search to be expanded by default','YesNo'), +('ExpireReservesAutoFill','0',NULL,'Automatically fill the next hold with a automatically canceled expired waiting hold.','YesNo'), +('ExpireReservesAutoFillEmail','', NULL,'If ExpireReservesAutoFill and an email is defined here, the email notification for the change in the hold will be sent to this address.','Free'), ('ExpireReservesMaxPickUpDelay','0','','Enabling this allows holds to expire automatically if they have not been picked by within the time period specified in ReservesMaxPickUpDelay','YesNo'), ('ExpireReservesMaxPickUpDelayCharge','0',NULL,'If ExpireReservesMaxPickUpDelay is enabled, and this field has a non-zero value, than a borrower whose waiting hold has expired will be charged this amount.','free'), ('ExpireReservesOnHolidays', '1', NULL, 'If false, reserves at a library will not be canceled on days the library is not open.', 'YesNo'), diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref index 3e81898de6..c7494ccf17 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref @@ -733,7 +733,17 @@ Circulation: choices: 1: Allow 0: "Don't allow" - - 'holds to expire automatically if they have not been picked by within the time period specified in the ReservesMaxPickUpDelay system preference.
NOTE: This system preference requires the misc/cronjobs/holds/cancel_expired_holds.pl cronjob. Ask your system administrator to schedule it.' + - 'holds to expire automatically if they have not been picked by within the time period specified in the ReservesMaxPickUpDelay system preference.' + - '
NOTE: This system preference requires the misc/cronjobs/holds/cancel_expired_holds.pl cronjob. Ask your system administrator to schedule it.
' + - pref: ExpireReservesAutoFill + choices: + 1: "Do" + 0: "Don't" + - automatically fill the next hold using the item. If enabled, an email notification will be sent to either the email address defined in ExpireReservesAutoFillEmail, the item's holding library's email address, or the email address defined in KohaAdminEmailAddress in that order. + - + - Send email notification of the new hold filled with a canceled item to + - pref: ExpireReservesAutoFillEmail + - . If no address is defined here, the email will be sent to either the item's holding library or the email address defined in KohaAdminEmailAddress in that order. - - If using ExpireReservesMaxPickUpDelay, charge a patron who allows their waiting hold to expire a fee of - pref: ExpireReservesMaxPickUpDelayCharge diff --git a/t/db_dependent/Holds/ExpireReservesAutoFill.t b/t/db_dependent/Holds/ExpireReservesAutoFill.t new file mode 100755 index 0000000000..610e3ba747 --- /dev/null +++ b/t/db_dependent/Holds/ExpireReservesAutoFill.t @@ -0,0 +1,190 @@ +#!/usr/bin/perl + +use Modern::Perl; + +use Test::More tests => 3; + +use t::lib::Mocks; +use t::lib::TestBuilder; + +use MARC::Record; + +use C4::Context; +use C4::Biblio; +use C4::Items; +use Koha::Database; +use Koha::Holds; + +BEGIN { + use FindBin; + use lib $FindBin::Bin; + use_ok('C4::Reserves'); +} + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $builder = t::lib::TestBuilder->new(); +my $dbh = C4::Context->dbh; + +# Create two random branches +my $library_1 = $builder->build({ source => 'Branch' })->{ branchcode }; +my $library_2 = $builder->build({ source => 'Branch' })->{ branchcode }; + +my $biblio = $builder->build_sample_biblio({ itemtype => 'DUMMY' }); +my $biblionumber = $biblio->id; + +# Create item instance for testing. +my $itemnumber = $builder->build_sample_item({ library => $library_1, biblionumber => $biblio->biblionumber })->itemnumber; + +my $patron_1 = $builder->build( { source => 'Borrower' } ); +my $patron_2 = $builder->build( { source => 'Borrower' } ); +my $patron_3 = $builder->build( { source => 'Borrower' } ); + +subtest 'Test automatically canceled expired waiting holds to fill the next hold, without a transfer' => sub { + plan tests => 10; + + $dbh->do('DELETE FROM reserves'); + $dbh->do('DELETE FROM message_queue'); + + # Add a hold on the item for each of our patrons + my $hold_1 = Koha::Hold->new( + { + priority => 0, + borrowernumber => $patron_1->{borrowernumber}, + branchcode => $library_1, + biblionumber => $biblionumber, + itemnumber => $itemnumber, + found => 'W', + reservedate => '1900-01-01', + waitingdate => '1900-01-01', + expirationdate => '1900-01-01', + lowestPriority => 0, + suspend => 0, + } + )->store(); + my $hold_2 = Koha::Hold->new( + { + priority => 1, + borrowernumber => $patron_2->{borrowernumber}, + branchcode => $library_1, + biblionumber => $biblionumber, + itemnumber => $itemnumber, + reservedate => '1900-01-01', + expirationdate => '9999-01-01', + lowestPriority => 0, + suspend => 0, + } + )->store(); + my $hold_3 = Koha::Hold->new( + { + priority => 2, + borrowernumber => $patron_2->{borrowernumber}, + branchcode => $library_1, + biblionumber => $biblionumber, + itemnumber => $itemnumber, + reservedate => '1900-01-01', + expirationdate => '9999-01-01', + lowestPriority => 0, + suspend => 0, + } + )->store(); + + # Test CancelExpiredReserves + t::lib::Mocks::mock_preference( 'ExpireReservesMaxPickUpDelay', 1 ); + t::lib::Mocks::mock_preference( 'ReservesMaxPickUpDelay', 1 ); + t::lib::Mocks::mock_preference( 'ExpireReservesOnHolidays', 1 ); + t::lib::Mocks::mock_preference( 'ExpireReservesAutoFill', 1 ); + t::lib::Mocks::mock_preference( 'ExpireReservesAutoFillEmail', + 'kyle@example.com' ); + + CancelExpiredReserves(); + + my @holds = Koha::Holds->search( {}, { order_by => 'priority' } ); + $hold_2 = $holds[0]; + $hold_3 = $holds[1]; + + is( @holds, 2, 'Found 2 holds' ); + is( $hold_2->priority, 0, 'Next hold in line now has priority of 0' ); + is( $hold_2->found, 'W', 'Next hold in line is now set to waiting' ); + + my @messages = $schema->resultset('MessageQueue') + ->search( { letter_code => 'HOLD_CHANGED' } ); + is( @messages, 1, 'Found 1 message in the message queue' ); + is( $messages[0]->to_address, 'kyle@example.com', 'Message sent to correct email address' ); + + $hold_2->expirationdate('1900-01-01')->store(); + + CancelExpiredReserves(); + + @holds = Koha::Holds->search( {}, { order_by => 'priority' } ); + $hold_3 = $holds[0]; + + is( @holds, 1, 'Found 1 hold' ); + is( $hold_3->priority, 0, 'Next hold in line now has priority of 0' ); + is( $hold_3->found, 'W', 'Next hold in line is now set to waiting' ); + + @messages = $schema->resultset('MessageQueue') + ->search( { letter_code => 'HOLD_CHANGED' } ); + is( @messages, 2, 'Found 2 messages in the message queue' ); + is( $messages[0]->to_address, 'kyle@example.com', 'Message sent to correct email address' ); +}; + +subtest 'Test automatically canceled expired waiting holds to fill the next hold, with a transfer' => sub { + plan tests => 5; + + $dbh->do('DELETE FROM reserves'); + $dbh->do('DELETE FROM message_queue'); + + # Add a hold on the item for each of our patrons + my $hold_1 = Koha::Hold->new( + { + priority => 0, + borrowernumber => $patron_1->{borrowernumber}, + branchcode => $library_1, + biblionumber => $biblionumber, + itemnumber => $itemnumber, + found => 'W', + reservedate => '1900-01-01', + waitingdate => '1900-01-01', + expirationdate => '1900-01-01', + lowestPriority => 0, + suspend => 0, + } + )->store(); + my $hold_2 = Koha::Hold->new( + { + priority => 1, + borrowernumber => $patron_2->{borrowernumber}, + branchcode => $library_2, + biblionumber => $biblionumber, + itemnumber => $itemnumber, + reservedate => '1900-01-01', + expirationdate => '9999-01-01', + lowestPriority => 0, + suspend => 0, + } + )->store(); + + # Test CancelExpiredReserves + t::lib::Mocks::mock_preference( 'ExpireReservesMaxPickUpDelay', 1 ); + t::lib::Mocks::mock_preference( 'ReservesMaxPickUpDelay', 1 ); + t::lib::Mocks::mock_preference( 'ExpireReservesOnHolidays', 1 ); + t::lib::Mocks::mock_preference( 'ExpireReservesAutoFill', 1 ); + t::lib::Mocks::mock_preference( 'ExpireReservesAutoFillEmail', + 'kyle@example.com' ); + + CancelExpiredReserves(); + + my @holds = Koha::Holds->search( {}, { order_by => 'priority' } ); + $hold_2 = $holds[0]; + + is( @holds, 1, 'Found 1 hold' ); + is( $hold_2->priority, 0, 'Next hold in line now has priority of 0' ); + is( $hold_2->found, 'T', 'Next hold in line is now set to in transit' ); + is( $hold_2->branchcode, $library_2, "Next hold in line has correct branchcode" ); + + my @messages = $schema->resultset('MessageQueue') + ->search( { letter_code => 'HOLD_CHANGED' } ); + is( @messages, 0, 'No messages in the message queue when generating transfer' ); +}; -- 2.30.2