@@ -, +, @@ your MARC frameworks to have this visible in the Editor) --- C4/Circulation.pm | 22 +++- catalogue/moredetail.pl | 2 + catalogue/updateitem.pl | 12 ++ cataloguing/additem.pl | 12 ++ ..._20844-add_RevertLostBibLevelHolds_syspref.perl | 6 + installer/data/mysql/sysprefs.sql | 2 + .../en/modules/admin/preferences/circulation.pref | 6 + .../prog/en/modules/catalogue/moredetail.tt | 13 +++ .../prog/en/modules/cataloguing/additem.tt | 19 ++++ t/db_dependent/Reserves.t | 124 ++++++++++++++++++++- 10 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 installer/data/mysql/atomicupdate/bug_20844-add_RevertLostBibLevelHolds_syspref.perl --- a/C4/Circulation.pm +++ a/C4/Circulation.pm @@ -3762,9 +3762,8 @@ sub ReturnLostItem{ MarkIssueReturned( $borrowernumber, $itemnum ); } - sub LostItem{ - my ($itemnumber, $mark_lost_from, $force_mark_returned) = @_; + my ($itemnumber, $mark_lost_from, $force_mark_returned, $cancel_reserve) = @_; unless ( $mark_lost_from ) { # Temporary check to avoid regressions @@ -3818,6 +3817,25 @@ sub LostItem{ Koha::Items->find($itemnumber)->holdingbranch($frombranch)->store; } my $transferdeleted = DeleteTransfer($itemnumber); + + # RevertLostBibLevelHolds + my $hold = Koha::Holds->find({ itemnumber => $itemnumber, found => { '!=' => undef } }); + if ( C4::Context->preference('RevertLostBibLevelHolds') && defined $hold ){ + # syspref is enabled and there is a waiting hold for this item + if ( $hold->item_level_hold && defined $cancel_reserve ){ + if ( $cancel_reserve ){ + # cancel item-level hold + $hold->cancel; + } else { # eq 'revert' + # revert item-level hold's waiting status + $hold = C4::Reserves::RevertWaitingStatus({ itemnumber => $itemnumber }); + } + } + if ( !$hold->item_level_hold ){ + # revert biblio-level hold's waiting status + $hold = C4::Reserves::RevertWaitingStatus({ itemnumber => $itemnumber }); + } + } } sub GetOfflineOperations { --- a/catalogue/moredetail.pl +++ a/catalogue/moredetail.pl @@ -237,6 +237,8 @@ foreach my $item (@items){ my $curr_borrower = Koha::Patrons->find( $item->{borrowernumber} ); $item->{patron} = $curr_borrower; } + + $item->{waiting_holds} = $item->{object}->holds({ found => 'W', item_level_hold => { '!=' => 0 } })->count; } my $mss = Koha::MarcSubfieldStructures->search({ frameworkcode => $fw, kohafield => 'items.itemlost', authorised_value => [ -and => {'!=' => undef }, {'!=' => ''}] }); --- a/catalogue/updateitem.pl +++ a/catalogue/updateitem.pl @@ -40,6 +40,18 @@ my $itemnotes=$cgi->param('itemnotes'); my $itemnotes_nonpublic=$cgi->param('itemnotes_nonpublic'); my $withdrawn=$cgi->param('withdrawn'); my $damaged=$cgi->param('damaged'); +my $cancelhold=$cgi->param('cancelhold'); +my $reverthold=$cgi->param('reverthold'); +my $redirecturl=$cgi->param('redirecturl'); +if ( defined $cancelhold ){ + LostItem($itemnumber, 'moredetail', 0, $cancelhold); +} elsif ( defined $reverthold ){ + $cancelhold = 0; + LostItem($itemnumber, 'moredetail', 0, $cancelhold); +} +if ( defined $redirecturl ){ + print $cgi->redirect( $redirecturl . $biblionumber ); +} my $confirm=$cgi->param('confirm'); my $dbh = C4::Context->dbh; --- a/cataloguing/additem.pl +++ a/cataloguing/additem.pl @@ -949,6 +949,18 @@ foreach my $tag ( keys %{$tagslib}){ } @loop_data = sort {$a->{subfield} cmp $b->{subfield} } @loop_data; +if ( C4::Context->preference('RevertLostBibLevelHolds') ){ + my @lostitems = Koha::Items->search({ biblionumber => $biblionumber, itemlost => { '!=' => 0 } }); + # find if any of these lost items have waiting holds + my @waitinglostitems; + foreach my $lostitem ( @lostitems ){ + if ( $lostitem->holds({ found => 'W', item_level_hold => 1 })->count > 0 ){ + push( @waitinglostitems, $lostitem ); + } + } + $template->param( waitinglostitems => \@waitinglostitems ); +} + my $item = Koha::Items->find($itemnumber); # We certainly want to fetch it earlier # what's the next op ? it's what we are not in : an add if we're editing, otherwise, and edit. --- a/installer/data/mysql/atomicupdate/bug_20844-add_RevertLostBibLevelHolds_syspref.perl +++ a/installer/data/mysql/atomicupdate/bug_20844-add_RevertLostBibLevelHolds_syspref.perl @@ -0,0 +1,6 @@ +$DBversion = 'XXX'; +if( CheckVersion( $DBversion ) ) { + $dbh->do(q{INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES ('RevertLostBibLevelHolds', '0', NULL, 'If an item is marked as lost after being allocated for a biblio-level hold, revert the hold from its waiting status', 'YesNo') }); + + NewVersion( $DBversion, 20844, "Add RevertLostBibLevelHolds system preference"); +} --- a/installer/data/mysql/sysprefs.sql +++ a/installer/data/mysql/sysprefs.sql @@ -542,6 +542,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('ReturnBeforeExpiry','0',NULL,'If ON, checkout will be prevented if returndate is after patron card expiry','YesNo'), ('ReturnLog','1',NULL,'If ON, enables the circulation (returns) log','YesNo'), ('ReturnpathDefault','',NULL,'Use this email address as return path or bounce address for undeliverable emails','Free'), +('RevertLostBibLevelHolds', '0', NULL, 'If an item is marked as lost after being allocated for a biblio-level hold, revert the hold from its waiting status', 'YesNo'), ('reviewson','1','','If ON, enables patron reviews of bibliographic records in the OPAC','YesNo'), ('RisExportAdditionalFields', '', NULL , 'Define additional RIS tags to export from MARC records in YAML format as an associative array with either a marc tag/subfield combination as the value, or a list of tag/subfield combinations.', 'textarea'), ('RoundFinesAtPayment','0', NULL,'If enabled any fines with fractions of a cent will be rounded to the nearest cent when payments are coll ected. e.g. 1.004 will be paid off by a 1.00 payment','YesNo'), @@ -565,6 +566,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('SelfCheckReceiptPrompt','1','NULL','If ON, print receipt dialog pops up when self checkout is finished','YesNo'), ('SelfCheckTimeout','120','','Define the number of seconds before the Web-based Self Checkout times out a patron','Integer'), ('SendAllEmailsTo','',NULL,'All emails will be redirected to this email if it is not empty','free'), +('SendLostHoldNotices', '0', NULL, 'Send a notice to a borrower if their reserved and waiting item is marked as lost', 'YesNo'), ('SeparateHoldings','0',NULL,'Separate current branch holdings from other holdings','YesNo'), ('SeparateHoldingsBranch','homebranch','homebranch|holdingbranch','Branch used to separate holdings','Choice'), ('SessionRestrictionByIP','1','Check for change in remote IP address for session security. Disable only when remote IP address changes frequently.','','YesNo'), --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref @@ -597,6 +597,12 @@ Circulation: - the last patron to return an item. This setting is independent of opacreadinghistory/AnonymousPatron. Holds Policy: - + - pref: RevertLostBibLevelHolds + choices: + yes: Revert + no: "Don't revert" + - waiting status of a hold if the allocated item is marked as Lost. + - - In the staff client, split the holds queue into separate tables by - pref: HoldsSplitQueue choices: --- a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt @@ -55,6 +55,19 @@ [% FOREACH ITEM_DAT IN ITEM_DATA %]

Barcode [% ITEM_DAT.barcode | html %] [% IF ( ITEM_DAT.notforloantext ) %][% ITEM_DAT.notforloantext | html %] [% END %]

+ + [% IF ITEM_DAT.itemlost and ITEM_DAT.waiting_holds > 0 %] +
+ This item has a waiting item-level hold on it, and has been marked as lost.
+
+ + + + +
+
+ [% END %] +

Item information [% IF ( CAN_user_editcatalogue_edit_catalogue ) %][% UNLESS ( ITEM_DAT.nomod ) %] [% IF ( CAN_user_editcatalogue_edit_items ) %] Edit item --- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/additem.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/additem.tt @@ -42,6 +42,25 @@ [% IF ( not_same_branch ) %]
Cannot delete: The items do not belong to your library.
[% END %] [% IF ( linked_analytics ) %]
Cannot delete: item has linked analytics..
[% END %] +[% IF waitinglostitems %] +
+ The following item(s) have a waiting item-level hold and have been marked as lost. +
    + [% FOREACH i IN waitinglostitems %] +
    +
  • Item ([% i.barcode %]) +
    + + + + +
  • + + [% END %] +
+
+[% END %] +
[% IF ( item_loop ) %]
--- a/t/db_dependent/Reserves.t +++ a/t/db_dependent/Reserves.t @@ -17,7 +17,7 @@ use Modern::Perl; -use Test::More tests => 62; +use Test::More tests => 63; use Test::MockModule; use Test::Warn; @@ -1035,6 +1035,128 @@ subtest 'MoveReserve additional test' => sub { }; +subtest 'Waiting item is marked as lost' => sub { + + plan tests => 15; + + # Set up data + my $biblio = $builder->build_sample_biblio(); + my $item = $builder->build_sample_item({ biblionumber => $biblio->biblionumber }); + my $patron = $builder->build_object({ class => 'Koha::Patrons' }); + + ## TEST 1: BIBLIO-LEVEL HOLD + + # place biblio-level hold + my $reserve_id = AddReserve({ + branchcode => $item->homebranch, + borrowernumber => $patron->borrowernumber, + biblionumber => $biblio->biblionumber, + }); + my $hold = Koha::Holds->find( $reserve_id ); + + is( $hold->item_level_hold, 0, 'Biblio-level hold placed' ); + + # set hold as waiting and assign item + $hold->set_waiting; + $hold->set({ itemnumber => $item->itemnumber, priority => 0 })->store; + + # mark item in biblio as lost + $item->itemlost( 1 )->store; + is( $item->itemlost, 1, 'Item assigned to waiting hold has been marked lost'); + + # do test + t::lib::Mocks::mock_preference( 'RevertLostBibLevelHolds', 0 ); + C4::Circulation::LostItem( $item->itemnumber, 'test', 0, 0 ); + is( $hold->found, 'W', 'Hold is still waiting even though the item is lost because RevertLostBibLevelHolds is disabled' ); + + # enable preference + t::lib::Mocks::mock_preference( 'RevertLostBibLevelHolds', 1 ); + + # try again + is( $item->itemlost, 1, 'Item assigned to waiting hold has been marked lost'); + C4::Circulation::LostItem( $item->itemnumber, 'test', 0, 0 ); + $hold = Koha::Holds->find( $reserve_id ); + is( $hold->found, undef, 'Hold waiting status has been reverted because RevertLostBibLevelHolds is enabled' ); + + # clean up + $hold->cancel; + $item->itemlost( 0 ); + + ## TEST 2: ITEM-LEVEL HOLD, REVERT + + # place item-level hold + $reserve_id = AddReserve({ + branchcode => $item->homebranch, + borrowernumber => $patron->borrowernumber, + biblionumber => $biblio->biblionumber, + itemnumber => $item->itemnumber + }); + $hold = Koha::Holds->find( $reserve_id ); + + is( $hold->item_level_hold, 1, 'Item-level hold placed' ); + + # set hold as waiting and assign item + $hold->set_waiting; + $hold->set({ itemnumber => $item->itemnumber, priority => 0 })->store; + + # mark item in biblio as lost + $item->itemlost( 1 ); + is( $item->itemlost, 1, 'Item assigned to waiting hold has been marked lost'); + + # do test + t::lib::Mocks::mock_preference( 'RevertLostBibLevelHolds', 0 ); + C4::Circulation::LostItem( $item->itemnumber, 'test', 0, 0 ); + is( $hold->found, 'W', 'Hold is still waiting even though the item is lost because RevertLostBibLevelHolds is disabled' ); + + # enable preference + t::lib::Mocks::mock_preference( 'RevertLostBibLevelHolds', 1 ); + + # try again + is( $item->itemlost, 1, 'Item assigned to waiting hold has been marked lost'); + C4::Circulation::LostItem( $item->itemnumber, 'test', 0, 0 ); + $hold = Koha::Holds->find( $reserve_id ); + is( $hold->found, undef, 'Hold waiting status has been reverted because RevertLostBibLevelHolds is enabled, and we chose not to cancel' ); + + # clean up + $hold->cancel; + $item->itemlost( 0 ); + + # TEST 3: ITEM-LEVEL HOLD, CANCEL + + # place item-level hold + $reserve_id = AddReserve({ + branchcode => $item->homebranch, + borrowernumber => $patron->borrowernumber, + biblionumber => $biblio->biblionumber, + itemnumber => $item->itemnumber + }); + $hold = Koha::Holds->find( $reserve_id ); + + is( $hold->item_level_hold, 1, 'Item-level hold placed' ); + + # set hold as waiting and assign item + $hold->set_waiting; + $hold->set({ itemnumber => $item->itemnumber, priority => 0 })->store; + + # mark item in biblio as lost + $item->itemlost( 1 ); + is( $item->itemlost, 1, 'Item assigned to waiting hold has been marked lost'); + + # do test + t::lib::Mocks::mock_preference( 'RevertLostBibLevelHolds', 0 ); + C4::Circulation::LostItem( $item->itemnumber, 'test', 0, 1 ); + is( $hold->found, 'W', 'Hold is still waiting even though the item is lost because RevertLostBibLevelHolds is disabled' ); + + # enable preference + t::lib::Mocks::mock_preference( 'RevertLostBibLevelHolds', 1 ); + + # try again + is( $item->itemlost, 1, 'Item assigned to waiting hold has been marked lost'); + C4::Circulation::LostItem( $item->itemnumber, 'test', 0, 1 ); + $hold = Koha::Holds->find( $reserve_id ); + is( Koha::Holds->find( $reserve_id ), undef, 'Hold has been cancelled with RevertLostBibLevelHolds enabled' ); +}; + sub count_hold_print_messages { my $message_count = $dbh->selectall_arrayref(q{ SELECT COUNT(*) --