From b800c2e68d993897c10e9eb80a03b37ddd310b44 Mon Sep 17 00:00:00 2001 From: Aleisha Amohia Date: Wed, 22 Apr 2020 20:06:54 +0000 Subject: [PATCH] Bug 19532: Other objects used in recalls feature - biblio->recalls - biblio->can_be_recalled - item->recall - item->can_be_recalled - item->can_set_waiting_recall - item->check_recalls - patron->recalls - Biblio.RecallsCount and relevant tests - t/db_dependent/Stats.t - t/db_dependent/Koha/Item.t - t/db_dependent/Koha/Biblio.t - t/db_dependent/Koha/Patron.t - t/db_dependent/XSLT.t - t/db_dependent/Search.t - t/db_dependent/Holds.t - t/db_dependent/Circulation/transferbook.t - t/db_dependent/Circulation.t Signed-off-by: David Nind Signed-off-by: David Nind Signed-off-by: Marcel de Rooy Signed-off-by: Fridolin Somers --- C4/Circulation.pm | 122 +++++++++- C4/Reserves.pm | 5 + C4/Search.pm | 9 + C4/XSLT.pm | 8 +- Koha/Biblio.pm | 108 +++++++++ Koha/Item.pm | 176 +++++++++++++++ Koha/Patron.pm | 24 ++ Koha/Template/Plugin/Biblio.pm | 9 + t/db_dependent/Circulation.t | 261 +++++++++++++++++++++- t/db_dependent/Circulation/transferbook.t | 33 ++- t/db_dependent/Holds.t | 27 +++ t/db_dependent/Koha/Biblio.t | 118 +++++++++- t/db_dependent/Koha/Item.t | 186 +++++++++++++++ t/db_dependent/Koha/Patron.t | 53 ++++- t/db_dependent/Stats.t | 2 +- t/db_dependent/XSLT.t | 16 +- 16 files changed, 1146 insertions(+), 11 deletions(-) diff --git a/C4/Circulation.pm b/C4/Circulation.pm index bbddb00fb9..3945a5f6a8 100644 --- a/C4/Circulation.pm +++ b/C4/Circulation.pm @@ -299,6 +299,14 @@ The item was reserved. The value is a reference-to-hash whose keys are fields fr The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored. +=item C + +A recall for this item was found, and the transfer has already been completed as the item's branch matches the recall's pickup branch. + +=item C + +A recall for this item was found, and the item needs to be transferred to the recall's pickup branch. + =back =back @@ -372,6 +380,19 @@ sub transferbook { $dotransfer = 0 unless $ignoreRs; } + # find recall + my $recall = Koha::Recalls->find({ itemnumber => $itemnumber, status => 'T' }); + if ( defined $recall and C4::Context->preference('UseRecalls') ) { + # do a transfer if the recall branch is different to the item holding branch + if ( $recall->branchcode eq $fbr ) { + $dotransfer = 0; + $messages->{'RecallPlacedAtHoldingBranch'} = 1; + } else { + $dotransfer = 1; + $messages->{'RecallFound'} = $recall; + } + } + #actually do the transfer.... if ($dotransfer) { ModItemTransfer( $itemnumber, $fbr, $tbr, $trigger ); @@ -722,6 +743,10 @@ sticky due date is invalid or due date in the past if the borrower borrows to much things +=head3 RECALLED + +recalled by someone else + =cut sub CanBookBeIssued { @@ -1095,7 +1120,50 @@ sub CanBookBeIssued { } } - unless ( $ignore_reserves ) { + my $recall; + # CHECK IF ITEM HAS BEEN RECALLED BY ANOTHER PATRON + # Only bother doing this if UseRecalls is enabled and the item is recallable + # Don't look at recalls that are in transit + if ( C4::Context->preference('UseRecalls') and $item_object->can_be_waiting_recall ) { + my @recalls = $biblio->recalls; + + foreach my $r ( @recalls ) { + if ( $r->itemnumber and + $r->itemnumber == $item_object->itemnumber and + $r->borrowernumber == $patron->borrowernumber and + $r->waiting ) { + $messages{RECALLED} = $r->recall_id; + $recall = $r; + # this item is already waiting for this borrower and the recall can be fulfilled + last; + } + elsif ( $r->itemnumber and + $r->itemnumber == $item_object->itemnumber and + $r->in_transit ) { + # recalled item is in transit + $issuingimpossible{RECALLED_INTRANSIT} = $r->branchcode; + } + elsif ( $r->item_level_recall and + $r->itemnumber == $item_object->itemnumber and + $r->borrowernumber != $patron->borrowernumber and + !$r->in_transit ) { + # this specific item has been recalled by a different patron + $needsconfirmation{RECALLED} = $r; + $recall = $r; + last; + } + elsif ( !$r->item_level_recall and + $r->borrowernumber != $patron->borrowernumber and + !$r->in_transit ) { + # a different patron has placed a biblio-level recall and this item is eligible to fill it + $needsconfirmation{RECALLED} = $r; + $recall = $r; + last; + } + } + } + + unless ( $ignore_reserves and defined $recall ) { # See if the item is on reserve. my ( $restype, $res ) = C4::Reserves::CheckReserves( $item_object->itemnumber ); if ($restype) { @@ -1409,6 +1477,10 @@ AddIssue does the following things : * RESERVE PLACED ? - fill reserve if reserve to this patron - cancel reserve or not, otherwise + * RECALL PLACED ? + - fill recall if recall to this patron + - cancel recall or not + - revert recall's waiting status or not * TRANSFERT PENDING ? - complete the transfert * ISSUE THE BOOK @@ -1423,6 +1495,8 @@ sub AddIssue { my $onsite_checkout = $params && $params->{onsite_checkout} ? 1 : 0; my $switch_onsite_checkout = $params && $params->{switch_onsite_checkout}; my $auto_renew = $params && $params->{auto_renew}; + my $cancel_recall = $params && $params->{cancel_recall}; + my $recall_id = $params && $params->{recall_id}; my $dbh = C4::Context->dbh; my $barcodecheck = CheckValidBarcode($barcode); @@ -1496,6 +1570,8 @@ sub AddIssue { $item_object->discard_changes; } + Koha::Recalls->move_recall({ action => $cancel_recall, recall_id => $recall_id, itemnumber => $item_object->itemnumber, borrowernumber => $borrower->{borrowernumber} }) if C4::Context->preference('UseRecalls'); + C4::Reserves::MoveReserve( $item_object->itemnumber, $borrower->{'borrowernumber'}, $cancelreserve ); # Starting process for transfer job (checking transfert and validate it if we have one) @@ -1922,6 +1998,16 @@ Value 1 if return is successful. If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer. +=item C + +This item can fill a recall. The recall object is returned. If the recall pickup branch differs from +the branch this item is being returned at, C is also returned which contains this +branchcode. + +=item C + +This item has been transferred to this branch to fill a recall. The recall object is returned. + =back C<$iteminformation> is a reference-to-hash, giving information about the @@ -2198,6 +2284,17 @@ sub AddReturn { } } + # find recalls... + # check if this item is recallable first, which includes checking if UseRecalls syspref is enabled + my $recall = undef; + $recall = $item->check_recalls if $item->can_be_waiting_recall; + if ( defined $recall ) { + $messages->{RecallFound} = $recall; + if ( $recall->branchcode ne $branch ) { + $messages->{RecallNeedsTransfer} = $branch; + } + } + # find reserves..... # launch the Checkreserves routine to find any holds my ($resfound, $resrec); @@ -2257,13 +2354,22 @@ sub AddReturn { $request->status('RET') if $request; } + my $transfer_recall = Koha::Recalls->find({ itemnumber => $item->itemnumber, status => 'T' }); # all recalls that have triggered a transfer will have an allocated itemnumber + if ( $transfer_recall and + $transfer_recall->branchcode eq $branch and + C4::Context->preference('UseRecalls') ) { + $messages->{TransferredRecall} = $transfer_recall; + } + # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer if ( $validTransfer && !C4::RotatingCollections::isItemInAnyCollection( $item->itemnumber ) && ( $doreturn or $messages->{'NotIssued'} ) and !$resfound and ( $branch ne $returnbranch ) and not $messages->{'WrongTransfer'} - and not $messages->{'WasTransfered'} ) + and not $messages->{'WasTransfered'} + and not $messages->{TransferredRecall} + and not $messages->{RecallNeedsTransfer} ) { my $BranchTransferLimitsType = C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ? 'effective_itemtype' : 'ccode'; if (C4::Context->preference("AutomaticItemReturn" ) or @@ -2773,6 +2879,18 @@ sub CanBookBeRenewed { return ( 0, $auto_renew ) if $auto_renew =~ 'auto_too_much_oweing'; } + my $recall = undef; + $recall = $item->check_recalls if $item->can_be_waiting_recall; + if ( defined $recall ) { + if ( $recall->item_level_recall ) { + # item-level recall. check if this item is the recalled item, otherwise renewal will be allowed + return ( 0, 'recalled' ) if ( $recall->itemnumber == $item->itemnumber ); + } else { + # biblio-level recall, so only disallow renewal if the biblio-level recall has been fulfilled by a different item + return ( 0, 'recalled' ) unless ( $recall->waiting ); + } + } + my ( $resfound, $resrec, $possible_reserves ) = C4::Reserves::CheckReserves($itemnumber); # If next hold is non priority, then check if any hold with priority (non_priority = 0) exists for the same biblionumber. diff --git a/C4/Reserves.pm b/C4/Reserves.pm index 6e86857db8..8b5d7f3110 100644 --- a/C4/Reserves.pm +++ b/C4/Reserves.pm @@ -375,6 +375,7 @@ sub CanBookBeReserved{ { status => libraryNotPickupLocation }, if given branchcode is not configured to be a pickup location { status => cannotBeTransferred }, if branch transfer limit applies on given item and branchcode { status => pickupNotInHoldGroup }, pickup location is not in hold group, and pickup locations are only allowed from hold groups. + { status => recall }, if the borrower has already placed a recall on this item =cut @@ -420,6 +421,10 @@ sub CanItemBeReserved { return { status =>'alreadypossession' }; } + # check if a recall exists on this item from this borrower + return { status => 'recall' } + if Koha::Recalls->search({ borrowernumber => $borrowernumber, itemnumber => $itemnumber, old => undef })->count; + my $controlbranch = C4::Context->preference('ReservesControlBranch'); my $reserves_control_branch; diff --git a/C4/Search.pm b/C4/Search.pm index 96013e28dd..0d88d24ec3 100644 --- a/C4/Search.pm +++ b/C4/Search.pm @@ -1785,6 +1785,7 @@ sub searchResults { my $item_in_transit_count = 0; my $item_onhold_count = 0; my $notforloan_count = 0; + my $item_recalled_count = 0; my $items_count = scalar(@fields); my $maxitems_pref = C4::Context->preference('maxItemsinSearchResults'); my $maxitems = $maxitems_pref ? $maxitems_pref - 1 : 1; @@ -1876,6 +1877,9 @@ sub searchResults { # is item on the reserve shelf? my $reservestatus = ''; + # is item a waiting recall? + my $recallstatus = ''; + unless ($item->{withdrawn} || $item->{itemlost} || $item->{damaged} @@ -1897,6 +1901,7 @@ sub searchResults { # ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber}); $reservestatus = C4::Reserves::GetReserveStatus( $item->{itemnumber} ); + $recallstatus = 'Waiting' if Koha::Recalls->search({ itemnumber => $item->{itemnumber}, status => 'W' })->count; } # item is withdrawn, lost, damaged, not for loan, reserved or in transit @@ -1905,6 +1910,7 @@ sub searchResults { || $item->{damaged} || $item->{notforloan} || $reservestatus eq 'Waiting' + || $recallstatus eq 'Waiting' || ($transfertwhen && $transfertwhen ne '')) { $withdrawn_count++ if $item->{withdrawn}; @@ -1912,6 +1918,7 @@ sub searchResults { $itemdamaged_count++ if $item->{damaged}; $item_in_transit_count++ if $transfertwhen && $transfertwhen ne ''; $item_onhold_count++ if $reservestatus eq 'Waiting'; + $item_recalled_count++ if $recallstatus eq 'Waiting'; $item->{status} = ($item->{withdrawn}//q{}) . "-" . ($item->{itemlost}//q{}) . "-" . ($item->{damaged}//q{}) . "-" . ($item->{notforloan}//q{}); $other_count++; @@ -1921,6 +1928,7 @@ sub searchResults { $other_items->{$key}->{$_} = $item->{$_}; } $other_items->{$key}->{intransit} = ( $transfertwhen ne '' ) ? 1 : 0; + $other_items->{$key}->{recalled} = ($recallstatus) ? 1 : 0; $other_items->{$key}->{onhold} = ($reservestatus) ? 1 : 0; $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value and $item->{notforloan}; $other_items->{$key}->{count}++ if $item->{$hbranch}; @@ -2014,6 +2022,7 @@ sub searchResults { $oldbiblio->{damagedcount} = $itemdamaged_count; $oldbiblio->{intransitcount} = $item_in_transit_count; $oldbiblio->{onholdcount} = $item_onhold_count; + $oldbiblio->{recalledcount} = $item_recalled_count; $oldbiblio->{orderedcount} = $ordered_count; $oldbiblio->{notforloancount} = $notforloan_count; diff --git a/C4/XSLT.pm b/C4/XSLT.pm index fd4a3f41c0..2b484ccaf6 100644 --- a/C4/XSLT.pm +++ b/C4/XSLT.pm @@ -354,7 +354,13 @@ sub buildKohaItemsNamespace { my $status; my $substatus = ''; - if ($item->has_pending_hold) { + my $recalls = Koha::Recalls->search({ itemnumber => $item->itemnumber, status => 'W' }); + + if ( $recalls->count ) { + # recalls take priority over holds + $status = 'Waiting'; + } + elsif ( $item->has_pending_hold ) { $status = 'other'; $substatus = 'Pending hold'; } diff --git a/Koha/Biblio.pm b/Koha/Biblio.pm index c4874a859a..2c44a4a006 100644 --- a/Koha/Biblio.pm +++ b/Koha/Biblio.pm @@ -1162,6 +1162,114 @@ sub get_marc_host { } } +=head3 recalls + + my @recalls = $biblio->recalls; + +Return all active recalls attached to this biblio, sorted by oldest first + +=cut + +sub recalls { + my ( $self ) = @_; + my @recalls_rs = Koha::Recalls->search({ biblionumber => $self->biblionumber, old => undef }, { order_by => { -asc => 'recalldate' } }); + return @recalls_rs; +} + +=head3 can_be_recalled + + my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object }); + +Does biblio-level checks and returns the items attached to this biblio that are available for recall + +=cut + +sub can_be_recalled { + my ( $self, $params ) = @_; + + return 0 if !( C4::Context->preference('UseRecalls') ); + + my $patron = $params->{patron}; + + my $branchcode = C4::Context->userenv->{'branch'}; + if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) { + $branchcode = $patron->branchcode; + } + + my @all_items = Koha::Items->search({ biblionumber => $self->biblionumber }); + + # if there are no available items at all, no recall can be placed + return 0 if ( scalar @all_items == 0 ); + + my @itemtypes; + my @itemnumbers; + my @items; + foreach my $item ( @all_items ) { + if ( $item->can_be_recalled({ patron => $patron }) ) { + push( @itemtypes, $item->effective_itemtype ); + push( @itemnumbers, $item->itemnumber ); + push( @items, $item ); + } + } + + # if there are no recallable items, no recall can be placed + return 0 if ( scalar @items == 0 ); + + # Check the circulation rule for each relevant itemtype for this biblio + my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls ); + foreach my $itemtype ( @itemtypes ) { + my $rule = Koha::CirculationRules->get_effective_rules({ + branchcode => $branchcode, + categorycode => $patron ? $patron->categorycode : undef, + itemtype => $itemtype, + rules => [ + 'recalls_allowed', + 'recalls_per_record', + 'on_shelf_recalls', + ], + }); + push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule; + push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule; + push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule; + } + my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest + my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest + my %on_shelf_recalls_count = (); + foreach my $count ( @on_shelf_recalls ) { + $on_shelf_recalls_count{$count}++; + } + my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common + + # check recalls allowed has been set and is not zero + return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 ); + + if ( $patron ) { + # check borrower has not reached open recalls allowed limit + return 0 if ( $patron->recalls->count >= $recalls_allowed ); + + # check borrower has not reached open recalls allowed per record limit + return 0 if ( $patron->recalls({ biblionumber => $self->biblionumber })->count >= $recalls_per_record ); + + # check if any of the items under this biblio are already checked out by this borrower + return 0 if ( Koha::Checkouts->search({ itemnumber => [ @itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 ); + } + + # check item availability + my $checked_out_count = 0; + foreach (@items) { + if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; } + } + + # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout + return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items ); + + # can't recall if no items have been checked out + return 0 if ( $checked_out_count == 0 ); + + # can recall + return @items; +} + =head2 Internal methods =head3 type diff --git a/Koha/Item.pm b/Koha/Item.pm index 26321babfe..2f5fd78050 100644 --- a/Koha/Item.pm +++ b/Koha/Item.pm @@ -1451,6 +1451,182 @@ sub _after_item_action_hooks { ); } +=head3 recall + + my $recall = $item->recall; + +Return the relevant recall for this item + +=cut + +sub recall { + my ( $self ) = @_; + my @recalls = Koha::Recalls->search({ biblionumber => $self->biblionumber, old => undef }, { order_by => { -asc => 'recalldate' } }); + foreach my $recall (@recalls) { + if ( $recall->item_level_recall and $recall->itemnumber == $self->itemnumber ){ + return $recall; + } + } + # no item-level recall to return, so return earliest biblio-level + # FIXME: eventually this will be based on priority + return $recalls[0]; +} + +=head3 can_be_recalled + + if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall + +Does item-level checks and returns if items can be recalled by this borrower + +=cut + +sub can_be_recalled { + my ( $self, $params ) = @_; + + return 0 if !( C4::Context->preference('UseRecalls') ); + + # check if this item is not for loan, withdrawn or lost + return 0 if ( $self->notforloan != 0 ); + return 0 if ( $self->itemlost != 0 ); + return 0 if ( $self->withdrawn != 0 ); + + # check if this item is not checked out - if not checked out, can't be recalled + return 0 if ( !defined( $self->checkout ) ); + + my $patron = $params->{patron}; + + my $branchcode = C4::Context->userenv->{'branch'}; + if ( $patron ) { + $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed ); + } + + # Check the circulation rule for each relevant itemtype for this item + my $rule = Koha::CirculationRules->get_effective_rules({ + branchcode => $branchcode, + categorycode => $patron ? $patron->categorycode : undef, + itemtype => $self->effective_itemtype, + rules => [ + 'recalls_allowed', + 'recalls_per_record', + 'on_shelf_recalls', + ], + }); + + # check recalls allowed has been set and is not zero + return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 ); + + if ( $patron ) { + # check borrower has not reached open recalls allowed limit + return 0 if ( $patron->recalls->count >= $rule->{recalls_allowed} ); + + # check borrower has not reach open recalls allowed per record limit + return 0 if ( $patron->recalls({ biblionumber => $self->biblionumber })->count >= $rule->{recalls_per_record} ); + + # check if this patron has already recalled this item + return 0 if ( Koha::Recalls->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber, old => undef })->count > 0 ); + + # check if this patron has already checked out this item + return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 ); + + # check if this patron has already reserved this item + return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 ); + } + + # check item availability + # items are unavailable for recall if they are lost, withdrawn or notforloan + my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 }); + + # if there are no available items at all, no recall can be placed + return 0 if ( scalar @items == 0 ); + + my $checked_out_count = 0; + foreach (@items) { + if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; } + } + + # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout + return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items ); + + # can't recall if no items have been checked out + return 0 if ( $checked_out_count == 0 ); + + # can recall + return 1; +} + +=head3 can_be_waiting_recall + + if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall + +Checks item type and branch of circ rules to return whether this item can be used to fill a recall. +At this point the item has already been recalled. We are now at the checkin and set waiting stage. + +=cut + +sub can_be_waiting_recall { + my ( $self ) = @_; + + return 0 if !( C4::Context->preference('UseRecalls') ); + + # check if this item is not for loan, withdrawn or lost + return 0 if ( $self->notforloan != 0 ); + return 0 if ( $self->itemlost != 0 ); + return 0 if ( $self->withdrawn != 0 ); + + my $branchcode = $self->holdingbranch; + if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) { + $branchcode = C4::Context->userenv->{'branch'}; + } else { + $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch; + } + + # Check the circulation rule for each relevant itemtype for this item + my $rule = Koha::CirculationRules->get_effective_rules({ + branchcode => $branchcode, + categorycode => undef, + itemtype => $self->effective_itemtype, + rules => [ + 'recalls_allowed', + ], + }); + + # check recalls allowed has been set and is not zero + return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 ); + + # can recall + return 1; +} + +=head3 check_recalls + + my $recall = $item->check_recalls; + +Get the most relevant recall for this item. + +=cut + +sub check_recalls { + my ( $self ) = @_; + + my @recalls = Koha::Recalls->search({ biblionumber => $self->biblionumber, itemnumber => [ $self->itemnumber, undef ], status => [ 'R','O','W','T' ] }, { order_by => { -asc => 'recalldate' } }); + + my $recall; + # iterate through relevant recalls to find the best one. + # if we come across a waiting recall, use this one. + # if we have iterated through all recalls and not found a waiting recall, use the first recall in the array, which should be the oldest recall. + foreach my $r ( @recalls ) { + if ( $r->waiting ) { + $recall = $r; + last; + } + } + unless ( defined $recall ) { + $recall = $recalls[0]; + } + + return $recall; +} + =head3 _type =cut diff --git a/Koha/Patron.pm b/Koha/Patron.pm index 9968d36350..5f9a38f953 100644 --- a/Koha/Patron.pm +++ b/Koha/Patron.pm @@ -2054,6 +2054,30 @@ sub safe_to_delete { return Koha::Result::Boolean->new(1); } +=head3 recalls + + my $recalls = $patron->recalls; + + my $recalls = $patron->recalls({ biblionumber => $biblionumber }); + +Return the patron's active recalls - total, or on a specific biblio + +=cut + +sub recalls { + my ( $self, $params ) = @_; + + my $biblionumber = $params->{biblionumber}; + + my $recalls_rs = Koha::Recalls->search({ borrowernumber => $self->borrowernumber, old => undef }); + + if ( $biblionumber ) { + $recalls_rs = Koha::Recalls->search({ borrowernumber => $self->borrowernumber, old => undef, biblionumber => $biblionumber }); + } + + return $recalls_rs; +} + =head2 Internal methods =head3 _type diff --git a/Koha/Template/Plugin/Biblio.pm b/Koha/Template/Plugin/Biblio.pm index ecb8de8261..59030574c1 100644 --- a/Koha/Template/Plugin/Biblio.pm +++ b/Koha/Template/Plugin/Biblio.pm @@ -26,6 +26,7 @@ use Koha::Holds; use Koha::Biblios; use Koha::Patrons; use Koha::ArticleRequests; +use Koha::Recalls; sub HoldsCount { my ( $self, $biblionumber ) = @_; @@ -56,4 +57,12 @@ sub CanArticleRequest { return $biblio ? $biblio->can_article_request( $borrower ) : 0; } +sub RecallsCount { + my ( $self, $biblionumber ) = @_; + + my $recalls = Koha::Recalls->search({ biblionumber => $biblionumber, old => undef }); + + return $recalls->count; +} + 1; diff --git a/t/db_dependent/Circulation.t b/t/db_dependent/Circulation.t index 249e8aee1c..5f2a567729 100755 --- a/t/db_dependent/Circulation.t +++ b/t/db_dependent/Circulation.t @@ -18,7 +18,7 @@ use Modern::Perl; use utf8; -use Test::More tests => 57; +use Test::More tests => 60; use Test::Exception; use Test::MockModule; use Test::Deep qw( cmp_deeply ); @@ -418,7 +418,7 @@ subtest "GetIssuingCharges tests" => sub { my ( $reused_itemnumber_1, $reused_itemnumber_2 ); subtest "CanBookBeRenewed tests" => sub { - plan tests => 93; + plan tests => 97; C4::Context->set_preference('ItemsDeniedRenewal',''); # Generate test biblio @@ -1429,6 +1429,77 @@ subtest "CanBookBeRenewed tests" => sub { $item_3->itemcallnumber || '' ), "Account line description must not contain 'Lost Items ', but be title, barcode, itemcallnumber" ); + + # Recalls + t::lib::Mocks::mock_preference('UseRecalls', 1); + Koha::CirculationRules->set_rules({ + categorycode => undef, + branchcode => undef, + itemtype => undef, + rules => { + recalls_allowed => 10, + renewalsallowed => 5, + }, + }); + my $recall_borrower = $builder->build_object({ class => 'Koha::Patrons' }); + my $recall_biblio = $builder->build_object({ class => 'Koha::Biblios' }); + my $recall_item1 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $recall_biblio->biblionumber } }); + my $recall_item2 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $recall_biblio->biblionumber } }); + + AddIssue( $renewing_borrower, $recall_item1->barcode ); + + # item-level and this item: renewal not allowed + my $recall = Koha::Recall->new({ + biblionumber => $recall_item1->biblionumber, + itemnumber => $recall_item1->itemnumber, + borrowernumber => $recall_borrower->borrowernumber, + branchcode => $recall_borrower->branchcode, + item_level_recall => 1, + status => 'R', + })->store; + ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $recall_item1->itemnumber ); + is( $error, 'recalled', 'Cannot renew item that has been recalled' ); + $recall->set_cancelled; + + # biblio-level requested recall: renewal not allowed + $recall = Koha::Recall->new({ + biblionumber => $recall_item1->biblionumber, + itemnumber => undef, + borrowernumber => $recall_borrower->borrowernumber, + branchcode => $recall_borrower->branchcode, + item_level_recall => 0, + status => 'R', + })->store; + ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $recall_item1->itemnumber ); + is( $error, 'recalled', 'Cannot renew item if biblio is recalled and has no item allocated' ); + $recall->set_cancelled; + + # item-level and not this item: renewal allowed + $recall = Koha::Recall->new({ + biblionumber => $recall_item2->biblionumber, + itemnumber => $recall_item2->itemnumber, + borrowernumber => $recall_borrower->borrowernumber, + branchcode => $recall_borrower->branchcode, + item_level_recall => 1, + status => 'R', + })->store; + ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $recall_item1->itemnumber ); + is( $renewokay, 1, 'Can renew item if item-level recall on biblio is not on this item' ); + $recall->set_cancelled; + + # biblio-level waiting recall: renewal allowed + $recall = Koha::Recall->new({ + biblionumber => $recall_item1->biblionumber, + itemnumber => undef, + borrowernumber => $recall_borrower->borrowernumber, + branchcode => $recall_borrower->branchcode, + item_level_recall => 0, + status => 'R', + })->store; + $recall->set_waiting({ item => $recall_item1 }); + ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $recall_item1->itemnumber ); + is( $renewokay, 1, 'Can renew item if biblio-level recall has already been allocated an item' ); + $recall->set_cancelled; }; subtest "GetUpcomingDueIssues" => sub { @@ -1909,6 +1980,68 @@ subtest 'AddIssue & AllowReturnToBranch' => sub { # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch'); }; +subtest 'AddIssue | recalls' => sub { + plan tests => 3; + + t::lib::Mocks::mock_preference("UseRecalls", 1); + t::lib::Mocks::mock_preference("item-level_itypes", 1); + my $patron1 = $builder->build_object({ class => 'Koha::Patrons' }); + my $patron2 = $builder->build_object({ class => 'Koha::Patrons' }); + my $item = $builder->build_sample_item; + Koha::CirculationRules->set_rules({ + branchcode => undef, + itemtype => undef, + categorycode => undef, + rules => { + recalls_allowed => 10, + }, + }); + + # checking out item that they have recalled + my $recall1 = Koha::Recall->new({ + borrowernumber => $patron1->borrowernumber, + biblionumber => $item->biblionumber, + itemnumber => $item->itemnumber, + item_level_recall => 1, + branchcode => $patron1->branchcode, + status => 'R', + })->store; + AddIssue( $patron1->unblessed, $item->barcode, undef, undef, undef, undef, { recall_id => $recall1->recall_id } ); + $recall1 = Koha::Recalls->find( $recall1->recall_id ); + is( $recall1->finished, 1, 'Recall was fulfilled when patron checked out item' ); + AddReturn( $item->barcode, $item->homebranch ); + + # this item is has a recall request. cancel recall + my $recall2 = Koha::Recall->new({ + borrowernumber => $patron2->borrowernumber, + biblionumber => $item->biblionumber, + itemnumber => $item->itemnumber, + item_level_recall => 1, + branchcode => $patron2->branchcode, + status => 'R', + })->store; + AddIssue( $patron1->unblessed, $item->barcode, undef, undef, undef, undef, { recall_id => $recall2->recall_id, cancel_recall => 'cancel' } ); + $recall2 = Koha::Recalls->find( $recall2->recall_id ); + is( $recall2->cancelled, 1, 'Recall was cancelled when patron checked out item' ); + AddReturn( $item->barcode, $item->homebranch ); + + # this item is waiting to fulfill a recall. revert recall + my $recall3 = Koha::Recall->new({ + borrowernumber => $patron2->borrowernumber, + biblionumber => $item->biblionumber, + itemnumber => $item->itemnumber, + item_level_recall => 1, + branchcode => $patron2->branchcode, + status => 'R', + })->store; + $recall3->set_waiting; + AddIssue( $patron1->unblessed, $item->barcode, undef, undef, undef, undef, { recall_id => $recall3->recall_id, cancel_recall => 'revert' } ); + $recall3 = Koha::Recalls->find( $recall3->recall_id ); + is( $recall3->requested, 1, 'Recall was reverted from waiting when patron checked out item' ); + AddReturn( $item->barcode, $item->homebranch ); +}; + + subtest 'CanBookBeIssued + Koha::Patron->is_debarred|has_overdues' => sub { plan tests => 8; @@ -3827,6 +3960,70 @@ subtest 'CanBookBeIssued | notforloan' => sub { # TODO test with AllowNotForLoanOverride = 1 }; +subtest 'CanBookBeIssued | recalls' => sub { + plan tests => 3; + + t::lib::Mocks::mock_preference("UseRecalls", 1); + t::lib::Mocks::mock_preference("item-level_itypes", 1); + my $patron1 = $builder->build_object({ class => 'Koha::Patrons' }); + my $patron2 = $builder->build_object({ class => 'Koha::Patrons' }); + my $item = $builder->build_sample_item; + Koha::CirculationRules->set_rules({ + branchcode => undef, + itemtype => undef, + categorycode => undef, + rules => { + recalls_allowed => 10, + }, + }); + + # item-level recall + my $recall = Koha::Recall->new({ + borrowernumber => $patron1->borrowernumber, + biblionumber => $item->biblionumber, + itemnumber => $item->itemnumber, + item_level_recall => 1, + branchcode => $patron1->branchcode, + status => 'R', + })->store; + + my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron2, $item->barcode, undef, undef, undef, undef ); + is( $needsconfirmation->{RECALLED}->recall_id, $recall->recall_id, "Another patron has placed an item-level recall on this item" ); + + $recall->set_cancelled; + + # biblio-level recall + $recall = Koha::Recall->new({ + borrowernumber => $patron1->borrowernumber, + biblionumber => $item->biblionumber, + itemnumber => undef, + item_level_recall => 0, + branchcode => $patron1->branchcode, + status => 'R', + })->store; + + ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron2, $item->barcode, undef, undef, undef, undef ); + is( $needsconfirmation->{RECALLED}->recall_id, $recall->recall_id, "Another patron has placed a biblio-level recall and this item is eligible to fill it" ); + + $recall->set_cancelled; + + # biblio-level recall + $recall = Koha::Recall->new({ + borrowernumber => $patron1->borrowernumber, + biblionumber => $item->biblionumber, + itemnumber => undef, + item_level_recall => 0, + branchcode => $patron1->branchcode, + status => 'R', + })->store; + $recall->set_waiting({ item => $item, expirationdate => dt_from_string() }); + + my ( undef, undef, undef, $messages ) = CanBookBeIssued( $patron1, $item->barcode, undef, undef, undef, undef ); + is( $messages->{RECALLED}, $recall->recall_id, "This book can be issued by this patron and they have placed a recall" ); + + $recall->set_cancelled; +}; + subtest 'AddReturn should clear items.onloan for unissued items' => sub { plan tests => 1; @@ -3842,6 +4039,66 @@ subtest 'AddReturn should clear items.onloan for unissued items' => sub { is( $item->onloan, undef, 'AddReturn did clear items.onloan' ); }; +subtest 'AddReturn | recalls' => sub { + plan tests => 3; + + t::lib::Mocks::mock_preference("UseRecalls", 1); + t::lib::Mocks::mock_preference("item-level_itypes", 1); + my $patron1 = $builder->build_object({ class => 'Koha::Patrons' }); + my $patron2 = $builder->build_object({ class => 'Koha::Patrons' }); + my $item1 = $builder->build_sample_item; + Koha::CirculationRules->set_rules({ + branchcode => undef, + itemtype => undef, + categorycode => undef, + rules => { + recalls_allowed => 10, + }, + }); + + # this item can fill a recall with pickup at this branch + AddIssue( $patron1->unblessed, $item1->barcode ); + my $recall1 = Koha::Recall->new({ + borrowernumber => $patron2->borrowernumber, + biblionumber => $item1->biblionumber, + itemnumber => $item1->itemnumber, + item_level_recall => 1, + branchcode => $item1->homebranch, + status => 'R', + })->store; + my ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $item1->barcode, $item1->homebranch ); + is( $messages->{RecallFound}->recall_id, $recall1->recall_id, "Recall found" ); + $recall1->set_cancelled; + + # this item can fill a recall but needs transfer + AddIssue( $patron1->unblessed, $item1->barcode ); + $recall1 = Koha::Recall->new({ + borrowernumber => $patron2->borrowernumber, + biblionumber => $item1->biblionumber, + itemnumber => $item1->itemnumber, + item_level_recall => 1, + branchcode => $patron2->branchcode, + status => 'R', + })->store; + ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $item1->barcode, $item1->homebranch ); + is( $messages->{RecallNeedsTransfer}, $item1->homebranch, "Recall requiring transfer found" ); + $recall1->set_cancelled; + + # this item is already in transit, do not ask to transfer + AddIssue( $patron1->unblessed, $item1->barcode ); + $recall1 = Koha::Recall->new({ + borrowernumber => $patron2->borrowernumber, + biblionumber => $item1->biblionumber, + itemnumber => $item1->itemnumber, + item_level_recall => 1, + branchcode => $patron2->branchcode, + status => 'R', + })->store; + $recall1->start_transfer; + ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $item1->barcode, $patron2->branchcode ); + is( $messages->{TransferredRecall}->recall_id, $recall1->recall_id, "In transit recall found" ); + $recall1->set_cancelled; +}; subtest 'AddRenewal and AddIssuingCharge tests' => sub { diff --git a/t/db_dependent/Circulation/transferbook.t b/t/db_dependent/Circulation/transferbook.t index 84756a82b8..c4de38e1ca 100755 --- a/t/db_dependent/Circulation/transferbook.t +++ b/t/db_dependent/Circulation/transferbook.t @@ -27,6 +27,9 @@ use Koha::DateUtils qw( dt_from_string ); use Koha::Item::Transfers; my $builder = t::lib::TestBuilder->new; +my $schema = Koha::Database->new->schema; + +$schema->storage->txn_begin; subtest 'transfer a non-existant item' => sub { plan tests => 2; @@ -101,7 +104,7 @@ subtest 'field population tests' => sub { #FIXME:'UseBranchTransferLimits tests missing subtest 'transfer already at destination' => sub { - plan tests => 5; + plan tests => 9; my $library = $builder->build_object( { class => 'Koha::Libraries' } )->store; t::lib::Mocks::mock_userenv( { branchcode => $library->branchcode } ); @@ -151,6 +154,33 @@ subtest 'transfer already at destination' => sub { is( $dotransfer, 0, 'Transfer of reserved item doesn\'t succeed without ignore_reserves' ); is( $messages->{ResFound}->{ResFound}, 'Reserved', "We found the reserve"); is( $messages->{ResFound}->{itemnumber}, $item->itemnumber, "We got the reserve info"); + + # recalls + t::lib::Mocks::mock_preference('UseRecalls', 1); + my $recall = Koha::Recall->new({ + biblionumber => $item->biblionumber, + itemnumber => $item->itemnumber, + item_level_recall => 1, + borrowernumber => $patron->borrowernumber, + branchcode => $library->branchcode, + status => 'R', + })->store; + ( $recall, $dotransfer, $messages ) = $recall->start_transfer; + is( $dotransfer, 0, 'Do not transfer recalled item, it has already arrived' ); + is( $messages->{RecallPlacedAtHoldingBranch}, 1, "We found the recall"); + + my $item2 = $builder->build_object({ class => 'Koha::Items' }); # this item will have a different holding branch to the pickup branch + $recall = Koha::Recall->new({ + biblionumber => $item2->biblionumber, + itemnumber => $item2->itemnumber, + item_level_recall => 1, + borrowernumber => $patron->borrowernumber, + branchcode => $library->branchcode, + status => 'R', + })->store; + ( $recall, $dotransfer, $messages ) = $recall->start_transfer; + is( $dotransfer, 1, 'Transfer of recalled item succeeded' ); + is( $messages->{RecallFound}->recall_id, $recall->recall_id, "We found the recall"); }; subtest 'transfer an issued item' => sub { @@ -301,3 +331,4 @@ subtest 'transferbook test from branch' => sub { is( $to_branch, $library->branchcode, 'The transfer is initiated to the specified branch'); }; +$schema->storage->txn_rollback; diff --git a/t/db_dependent/Holds.t b/t/db_dependent/Holds.t index 283ee0ae54..eef6c266eb 100755 --- a/t/db_dependent/Holds.t +++ b/t/db_dependent/Holds.t @@ -1549,7 +1549,34 @@ subtest 'non priority holds' => sub { is( $err, 'on_reserve', 'Item is on hold' ); $schema->storage->txn_rollback; +}; + +subtest 'CanItemBeReserved / recall' => sub { + plan tests => 1; + + $schema->storage->txn_begin; + + my $itemtype1 = $builder->build_object( { class => 'Koha::ItemTypes' } ); + my $library1 = $builder->build_object( { class => 'Koha::Libraries', value => {pickup_location => 1} } ); + my $patron1 = $builder->build_object( { class => 'Koha::Patrons', value => {branchcode => $library1->branchcode} } ); + my $biblio1 = $builder->build_sample_biblio({ itemtype => $itemtype1->itemtype }); + my $item1 = $builder->build_sample_item( + { + biblionumber => $biblio1->biblionumber, + library => $library1->branchcode + } + ); + Koha::Recall->new({ + borrowernumber => $patron1->borrowernumber, + biblionumber => $biblio1->biblionumber, + branchcode => $library1->branchcode, + itemnumber => $item1->itemnumber, + recalldate => '2020-05-04 10:10:10', + item_level_recall => 1, + })->store; + is( CanItemBeReserved( $patron1->borrowernumber, $item1->itemnumber, $library1->branchcode )->{status}, 'recall', "Can't reserve an item that they have already recalled" ); + $schema->storage->txn_rollback; }; subtest 'CanItemBeReserved rule precedence tests' => sub { diff --git a/t/db_dependent/Koha/Biblio.t b/t/db_dependent/Koha/Biblio.t index 7ba67b28e4..e52b7209ec 100755 --- a/t/db_dependent/Koha/Biblio.t +++ b/t/db_dependent/Koha/Biblio.t @@ -17,7 +17,7 @@ use Modern::Perl; -use Test::More tests => 20; +use Test::More tests => 21; # +1 use Test::Warn; use C4::Biblio qw( AddBiblio ModBiblio ModBiblioMarc ); @@ -880,6 +880,122 @@ subtest 'get_marc_authors() tests' => sub { $biblio = Koha::Biblios->find( $biblio->biblionumber ); is( 4, @{$biblio->get_marc_authors}, 'get_marc_authors retrieves correct number of author subfields' ); + $schema->storage->txn_rollback; +}; + +subtest 'Recalls tests' => sub { + + plan tests => 12; + + $schema->storage->txn_begin; + my $item1 = $builder->build_sample_item; + my $biblio = $item1->biblio; + my $branchcode = $item1->holdingbranch; + my $patron1 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } }); + my $patron2 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } }); + my $patron3 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } }); + my $item2 = $builder->build_object({ class => 'Koha::Items', value => { holdingbranch => $branchcode, homebranch => $branchcode, biblionumber => $biblio->biblionumber, itype => $item1->effective_itemtype } }); + t::lib::Mocks::mock_userenv({ patron => $patron1 }); + + my $recall1 = Koha::Recall->new({ + borrowernumber => $patron1->borrowernumber, + recalldate => Koha::DateUtils::dt_from_string, + biblionumber => $biblio->biblionumber, + branchcode => $branchcode, + status => 'R', + itemnumber => $item1->itemnumber, + expirationdate => undef, + item_level_recall => 1 + })->store; + my $recall2 = Koha::Recall->new({ + borrowernumber => $patron2->borrowernumber, + recalldate => Koha::DateUtils::dt_from_string, + biblionumber => $biblio->biblionumber, + branchcode => $branchcode, + status => 'R', + itemnumber => undef, + expirationdate => undef, + item_level_recall => 0 + })->store; + my $recall3 = Koha::Recall->new({ + borrowernumber => $patron3->borrowernumber, + recalldate => Koha::DateUtils::dt_from_string, + biblionumber => $biblio->biblionumber, + branchcode => $branchcode, + status => 'R', + itemnumber => $item1->itemnumber, + expirationdate => undef, + item_level_recall => 1 + })->store; + + my $recalls_count = scalar $biblio->recalls; + is( $recalls_count, 3, 'Correctly get number of active recalls for biblio' ); + + $recall1->set_cancelled; + $recall2->set_expired({ interface => 'COMMANDLINE' }); + + $recalls_count = scalar $biblio->recalls; + is( $recalls_count, 1, 'Correctly get number of active recalls for biblio' ); + + t::lib::Mocks::mock_preference('UseRecalls', 0); + is( $biblio->can_be_recalled({ patron => $patron1 }), 0, "Can't recall with UseRecalls disabled" ); + + t::lib::Mocks::mock_preference("UseRecalls", 1); + $item1->update({ notforloan => 1 }); + is( $biblio->can_be_recalled({ patron => $patron1 }), 0, "Can't recall with no available items" ); + + $item1->update({ notforloan => 0 }); + Koha::CirculationRules->set_rules({ + branchcode => $branchcode, + categorycode => $patron1->categorycode, + itemtype => $item1->effective_itemtype, + rules => { + recalls_allowed => 0, + recalls_per_record => 1, + on_shelf_recalls => 'all', + }, + }); + is( $biblio->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if recalls_allowed = 0" ); + + Koha::CirculationRules->set_rules({ + branchcode => $branchcode, + categorycode => $patron1->categorycode, + itemtype => $item1->effective_itemtype, + rules => { + recalls_allowed => 1, + recalls_per_record => 1, + on_shelf_recalls => 'all', + }, + }); + is( $biblio->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has more existing recall(s) than recalls_allowed" ); + is( $biblio->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has more existing recall(s) than recalls_per_record" ); + + $recall1->set_cancelled; + C4::Circulation::AddIssue( $patron1->unblessed, $item2->barcode ); + is( $biblio->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has already checked out an item attached to this biblio" ); + + is( $biblio->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if on_shelf_recalls = all and items are still available" ); + + Koha::CirculationRules->set_rules({ + branchcode => $branchcode, + categorycode => $patron1->categorycode, + itemtype => $item1->effective_itemtype, + rules => { + recalls_allowed => 1, + recalls_per_record => 1, + on_shelf_recalls => 'any', + }, + }); + C4::Circulation::AddReturn( $item2->barcode, $branchcode ); + is( $biblio->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if no items are checked out" ); + + $recall2->set_cancelled; + C4::Circulation::AddIssue( $patron2->unblessed, $item2->barcode ); + C4::Circulation::AddIssue( $patron2->unblessed, $item1->barcode ); + is( $biblio->can_be_recalled({ patron => $patron1 }), 2, "Can recall two items" ); + + $item1->update({ withdrawn => 1 }); + is( $biblio->can_be_recalled({ patron => $patron1 }), 1, "Can recall one item" ); $schema->storage->txn_rollback; }; diff --git a/t/db_dependent/Koha/Item.t b/t/db_dependent/Koha/Item.t index 0470a98c46..c1de0d0d68 100755 --- a/t/db_dependent/Koha/Item.t +++ b/t/db_dependent/Koha/Item.t @@ -1165,7 +1165,193 @@ subtest 'columns_to_str' => sub { $cache->clear_from_cache("MarcSubfieldStructure-"); $schema->storage->txn_rollback; +}; + +subtest 'Recalls tests' => sub { + + plan tests => 20; + + $schema->storage->txn_begin; + + my $item1 = $builder->build_sample_item; + my $biblio = $item1->biblio; + my $branchcode = $item1->holdingbranch; + my $patron1 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } }); + my $patron2 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } }); + my $patron3 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } }); + my $item2 = $builder->build_object({ class => 'Koha::Items', value => { holdingbranch => $branchcode, homebranch => $branchcode, biblionumber => $biblio->biblionumber, itype => $item1->effective_itemtype } }); + t::lib::Mocks::mock_userenv({ patron => $patron1 }); + + my $recall1 = Koha::Recall->new({ + borrowernumber => $patron1->borrowernumber, + recalldate => Koha::DateUtils::dt_from_string, + biblionumber => $biblio->biblionumber, + branchcode => $branchcode, + status => 'R', + itemnumber => $item1->itemnumber, + expirationdate => undef, + item_level_recall => 1 + })->store; + my $recall2 = Koha::Recall->new({ + borrowernumber => $patron2->borrowernumber, + recalldate => Koha::DateUtils::dt_from_string, + biblionumber => $biblio->biblionumber, + branchcode => $branchcode, + status => 'R', + itemnumber => $item1->itemnumber, + expirationdate => undef, + item_level_recall =>1 + })->store; + + is( $item1->recall->borrowernumber, $patron1->borrowernumber, 'Correctly returns most relevant recall' ); + + $recall2->set_cancelled; + + t::lib::Mocks::mock_preference('UseRecalls', 0); + is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall with UseRecalls disabled" ); + + t::lib::Mocks::mock_preference("UseRecalls", 1); + + $item1->update({ notforloan => 1 }); + is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall that is not for loan" ); + $item1->update({ notforloan => 0, itemlost => 1 }); + is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall that is marked lost" ); + $item1->update({ itemlost => 0, withdrawn => 1 }); + is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall that is withdrawn" ); + is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall item if not checked out" ); + + $item1->update({ withdrawn => 0 }); + C4::Circulation::AddIssue( $patron2->unblessed, $item1->barcode ); + + Koha::CirculationRules->set_rules({ + branchcode => $branchcode, + categorycode => $patron1->categorycode, + itemtype => $item1->effective_itemtype, + rules => { + recalls_allowed => 0, + recalls_per_record => 1, + on_shelf_recalls => 'all', + }, + }); + is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if recalls_allowed = 0" ); + + Koha::CirculationRules->set_rules({ + branchcode => $branchcode, + categorycode => $patron1->categorycode, + itemtype => $item1->effective_itemtype, + rules => { + recalls_allowed => 1, + recalls_per_record => 1, + on_shelf_recalls => 'all', + }, + }); + is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has more existing recall(s) than recalls_allowed" ); + is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has more existing recall(s) than recalls_per_record" ); + is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has already recalled this item" ); + + my $reserve_id = C4::Reserves::AddReserve({ branchcode => $branchcode, borrowernumber => $patron1->borrowernumber, biblionumber => $item1->biblionumber, itemnumber => $item1->itemnumber }); + is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall item if patron has already reserved it" ); + C4::Reserves::ModReserve({ rank => 'del', reserve_id => $reserve_id, branchcode => $branchcode, itemnumber => $item1->itemnumber, borrowernumber => $patron1->borrowernumber, biblionumber => $item1->biblionumber }); + + $recall1->set_cancelled; + is( $item1->can_be_recalled({ patron => $patron2 }), 0, "Can't recall if patron has already checked out an item attached to this biblio" ); + + is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if on_shelf_recalls = all and items are still available" ); + + Koha::CirculationRules->set_rules({ + branchcode => $branchcode, + categorycode => $patron1->categorycode, + itemtype => $item1->effective_itemtype, + rules => { + recalls_allowed => 1, + recalls_per_record => 1, + on_shelf_recalls => 'any', + }, + }); + C4::Circulation::AddReturn( $item1->barcode, $branchcode ); + is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if no items are checked out" ); + + C4::Circulation::AddIssue( $patron2->unblessed, $item1->barcode ); + is( $item1->can_be_recalled({ patron => $patron1 }), 1, "Can recall item" ); + + $recall1 = Koha::Recall->new({ + borrowernumber => $patron1->borrowernumber, + recalldate => Koha::DateUtils::dt_from_string, + biblionumber => $biblio->biblionumber, + branchcode => $branchcode, + status => 'R', + itemnumber => undef, + expirationdate => undef, + item_level_recall => 0 + })->store; + + # Patron2 has Item1 checked out. Patron1 has placed a biblio-level recall on Biblio1, so check if Item1 can fulfill Patron1's recall. + + Koha::CirculationRules->set_rules({ + branchcode => undef, + categorycode => undef, + itemtype => $item1->effective_itemtype, + rules => { + recalls_allowed => 0, + recalls_per_record => 1, + on_shelf_recalls => 'any', + }, + }); + is( $item1->can_be_waiting_recall, 0, "Recalls not allowed for this itemtype" ); + + Koha::CirculationRules->set_rules({ + branchcode => undef, + categorycode => undef, + itemtype => $item1->effective_itemtype, + rules => { + recalls_allowed => 1, + recalls_per_record => 1, + on_shelf_recalls => 'any', + }, + }); + is( $item1->can_be_waiting_recall, 1, "Recalls are allowed for this itemtype" ); + + # check_recalls tests + + $recall1 = Koha::Recall->new({ + borrowernumber => $patron2->borrowernumber, + recalldate => Koha::DateUtils::dt_from_string, + biblionumber => $biblio->biblionumber, + branchcode => $branchcode, + status => 'R', + itemnumber => $item1->itemnumber, + expirationdate => undef, + item_level_recall => 1 + })->store; + $recall2 = Koha::Recall->new({ + borrowernumber => $patron1->borrowernumber, + recalldate => Koha::DateUtils::dt_from_string, + biblionumber => $biblio->biblionumber, + branchcode => $branchcode, + status => 'R', + itemnumber => undef, + expirationdate => undef, + item_level_recall => 0 + })->store; + $recall2->set_waiting({ item => $item1 }); + + # return a waiting recall + my $check_recall = $item1->check_recalls; + is( $check_recall->borrowernumber, $patron1->borrowernumber, "Waiting recall is highest priority and returned" ); + + $recall2->revert_waiting; + + # return recall based on recalldate + $check_recall = $item1->check_recalls; + is( $check_recall->borrowernumber, $patron1->borrowernumber, "No waiting recall, so oldest recall is returned" ); + + $recall1->set_cancelled; + + # return a biblio-level recall + $check_recall = $item1->check_recalls; + is( $check_recall->borrowernumber, $patron1->borrowernumber, "Only remaining recall is returned" ); + $recall2->set_cancelled; }; subtest 'store() tests' => sub { diff --git a/t/db_dependent/Koha/Patron.t b/t/db_dependent/Koha/Patron.t index 819c3740f0..00d70e0187 100755 --- a/t/db_dependent/Koha/Patron.t +++ b/t/db_dependent/Koha/Patron.t @@ -19,7 +19,7 @@ use Modern::Perl; -use Test::More tests => 14; +use Test::More tests => 15; use Test::Exception; use Test::Warn; @@ -1048,6 +1048,57 @@ subtest 'messages' => sub { is( $messages->count, 2, "There are two messages for this patron" ); is( $messages->next->message, $message_1->message ); is( $messages->next->message, $message_2->message ); + $schema->storage->txn_rollback; +}; + +subtest 'recalls() tests' => sub { + + plan tests => 2; + my $biblio1 = $builder->build_object({ class => 'Koha::Biblios' }); + my $item1 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio1->biblionumber } }); + my $biblio2 = $builder->build_object({ class => 'Koha::Biblios' }); + my $item2 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio2->biblionumber } }); + + Koha::Recall->new({ + biblionumber => $biblio1->biblionumber, + borrowernumber => $patron->borrowernumber, + itemnumber => $item1->itemnumber, + branchcode => $patron->branchcode, + recalldate => dt_from_string, + status => 'R', + item_level_recall => 1, + })->store; + Koha::Recall->new({ + biblionumber => $biblio2->biblionumber, + borrowernumber => $patron->borrowernumber, + itemnumber => $item2->itemnumber, + branchcode => $patron->branchcode, + recalldate => dt_from_string, + status => 'R', + item_level_recall => 1, + })->store; + Koha::Recall->new({ + biblionumber => $biblio1->biblionumber, + borrowernumber => $patron->borrowernumber, + itemnumber => undef, + branchcode => $patron->branchcode, + recalldate => dt_from_string, + status => 'R', + item_level_recall => 0, + })->store; + my $recall = Koha::Recall->new({ + biblionumber => $biblio1->biblionumber, + borrowernumber => $patron->borrowernumber, + itemnumber => undef, + branchcode => $patron->branchcode, + recalldate => dt_from_string, + status => 'R', + item_level_recall => 0, + })->store; + $recall->set_cancelled; + + is( $patron->recalls->count, 3, "Correctly gets this patron's active recalls" ); + is( $patron->recalls({ biblionumber => $biblio1->biblionumber })->count, 2, "Correctly gets this patron's active recalls on a specific biblio" ); $schema->storage->txn_rollback; }; diff --git a/t/db_dependent/Stats.t b/t/db_dependent/Stats.t index 5bf1b615be..ce411905c5 100755 --- a/t/db_dependent/Stats.t +++ b/t/db_dependent/Stats.t @@ -55,7 +55,7 @@ $return_error = $@; isnt ($return_error,'',"UpdateStats returns undef and croaks if type is undef"); # returns undef and croaks if mandatory params are missing -my @allowed_circulation_types = qw (renew issue localuse return); +my @allowed_circulation_types = qw (renew issue localuse return onsite_checkout recall); my @allowed_accounts_types = qw (writeoff payment); my @circulation_mandatory_keys = qw (branch borrowernumber itemnumber ccode itemtype); #don't check type here my @accounts_mandatory_keys = qw (branch borrowernumber amount); #don't check type here diff --git a/t/db_dependent/XSLT.t b/t/db_dependent/XSLT.t index 9cda1c5b8c..b56ea7971e 100755 --- a/t/db_dependent/XSLT.t +++ b/t/db_dependent/XSLT.t @@ -48,7 +48,7 @@ subtest 'transformMARCXML4XSLT tests' => sub { }; subtest 'buildKohaItemsNamespace status tests' => sub { - plan tests => 16; + plan tests => 17; t::lib::Mocks::mock_preference('Reference_NFL_Statuses', '1|2'); t::lib::Mocks::mock_preference( 'OPACResultsLibrary', 'holdingbranch' ); @@ -131,7 +131,8 @@ subtest 'buildKohaItemsNamespace status tests' => sub { } }); $xml = C4::XSLT::buildKohaItemsNamespace( $item->biblionumber,[]); - like($xml,qr{Waiting},"Waiting status takes precedence over In transit"); + like($xml,qr{Waiting},"Waiting status takes precedence over In transit (holds)"); + $hold->cancel; $builder->build({ source => "TmpHoldsqueue", value => { itemnumber => $item->itemnumber @@ -141,6 +142,17 @@ subtest 'buildKohaItemsNamespace status tests' => sub { like($xml,qr{Pending hold},"Pending status takes precedence over all"); my $library_name = $holdinglibrary->branchname; like($xml,qr{${library_name}}, "Found resultbranch / holding branch" ); + + my $recall = $builder->build_object({ class => 'Koha::Recalls', value => { + biblionumber => $item->biblionumber, + itemnumber => $item->itemnumber, + branchcode => $item->holdingbranch, + status => 'R', + }}); + $recall->set_waiting; + $xml = C4::XSLT::buildKohaItemsNamespace( $item->biblionumber,[]); + like($xml,qr{Waiting},"Waiting status takes precedence over In transit (recalls)"); + }; $schema->storage->txn_rollback; -- 2.39.5