Bugzilla – Attachment 111204 Details for
Bug 19532
Recalls for Koha
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 19532: Other objects used in recalls feature
Bug-19532-Other-objects-used-in-recalls-feature.patch (text/plain), 63.01 KB, created by
Aleisha Amohia
on 2020-10-05 00:28:51 UTC
(
hide
)
Description:
Bug 19532: Other objects used in recalls feature
Filename:
MIME Type:
Creator:
Aleisha Amohia
Created:
2020-10-05 00:28:51 UTC
Size:
63.01 KB
patch
obsolete
>From cc4c476108772572c5e085f064d8954cba4dbe96 Mon Sep 17 00:00:00 2001 >From: Aleisha Amohia <aleishaamohia@hotmail.com> >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 >--- > C4/Circulation.pm | 121 +++++++++++++- > 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 | 259 +++++++++++++++++++++++++++++- > t/db_dependent/Circulation/transferbook.t | 33 +++- > t/db_dependent/Holds.t | 29 +++- > t/db_dependent/Koha/Biblio.t | 121 +++++++++++++- > t/db_dependent/Koha/Item.t | 189 +++++++++++++++++++++- > t/db_dependent/Koha/Patron.t | 58 ++++++- > t/db_dependent/Stats.t | 2 +- > t/db_dependent/XSLT.t | 14 +- > 16 files changed, 1152 insertions(+), 13 deletions(-) > >diff --git a/C4/Circulation.pm b/C4/Circulation.pm >index 5c05326123..4cd9651dd4 100644 >--- a/C4/Circulation.pm >+++ b/C4/Circulation.pm >@@ -305,6 +305,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<RecallPlacedAtHoldingBranch> >+ >+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<RecallFound> >+ >+A recall for this item was found, and the item needs to be transferred to the recall's pickup branch. >+ > =back > > =back >@@ -378,6 +386,19 @@ sub transferbook { > $dotransfer = 1; > } > >+ # 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 ); >@@ -723,6 +744,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 { >@@ -1081,7 +1106,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) { >@@ -1360,6 +1428,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 >@@ -1374,6 +1446,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); > >@@ -1445,6 +1519,8 @@ sub AddIssue { > AddReturn( $item_object->barcode, C4::Context->userenv->{'branch'} ); > } > >+ 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) >@@ -1852,6 +1928,16 @@ Value 1 if return is successful. > > If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer. > >+=item C<RecallFound> >+ >+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<RecallNeedsTransfer> is also returned which contains this >+branchcode. >+ >+=item C<TransferredRecall> >+ >+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 >@@ -2079,6 +2165,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); >@@ -2137,8 +2234,16 @@ 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 && !$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) ){ >+ if ($validTransfer && !$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) >+ and not $messages->{'WrongTransfer'} 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 > (C4::Context->preference("UseBranchTransferLimits") and >@@ -2765,6 +2870,18 @@ sub CanBookBeRenewed { > } > } > >+ 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, undef ) = 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 2422d4facd..077c3b87d2 100644 >--- a/C4/Reserves.pm >+++ b/C4/Reserves.pm >@@ -353,6 +353,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 > >@@ -386,6 +387,10 @@ sub CanItemBeReserved { > return { status =>'itemAlreadyOnHold' } > if Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->count(); > >+ # 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 $querycount = q{ >diff --git a/C4/Search.pm b/C4/Search.pm >index 5509a5b4cb..1778e98c75 100644 >--- a/C4/Search.pm >+++ b/C4/Search.pm >@@ -1892,6 +1892,7 @@ sub searchResults { > my $can_place_holds = 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; >@@ -1986,6 +1987,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} >@@ -2007,6 +2011,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 >@@ -2015,6 +2020,7 @@ sub searchResults { > || $item->{damaged} > || $item->{notforloan} > || $reservestatus eq 'Waiting' >+ || $recallstatus eq 'Waiting' > || ($transfertwhen && $transfertwhen ne '')) > { > $withdrawn_count++ if $item->{withdrawn}; >@@ -2022,6 +2028,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{}); > > # can place a hold on a item if >@@ -2043,6 +2050,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}; >@@ -2117,6 +2125,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 1b3941c7fb..884d7e5f73 100644 >--- a/C4/XSLT.pm >+++ b/C4/XSLT.pm >@@ -321,7 +321,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 = 'Pending hold'; > } > elsif ( $item->holds->waiting->count ) { >diff --git a/Koha/Biblio.pm b/Koha/Biblio.pm >index 830b9c4d73..a098890816 100644 >--- a/Koha/Biblio.pm >+++ b/Koha/Biblio.pm >@@ -816,6 +816,114 @@ sub to_api_mapping { > }; > } > >+=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 5b12115932..69f43824a5 100644 >--- a/Koha/Item.pm >+++ b/Koha/Item.pm >@@ -989,6 +989,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 25ceac1470..c09ffe8698 100644 >--- a/Koha/Patron.pm >+++ b/Koha/Patron.pm >@@ -1725,6 +1725,30 @@ sub to_api_mapping { > }; > } > >+=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 e9d7a73009..a4e6aab341 100644 >--- a/Koha/Template/Plugin/Biblio.pm >+++ b/Koha/Template/Plugin/Biblio.pm >@@ -27,6 +27,7 @@ use Koha::Biblios; > use Koha::Patrons; > use Koha::ArticleRequests; > use Koha::ArticleRequest::Status; >+use Koha::Recalls; > > sub HoldsCount { > my ( $self, $biblionumber ) = @_; >@@ -63,4 +64,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 d078ef1550..49039a8f1d 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 => 49; >+use Test::More tests => 52; > use Test::Exception; > use Test::MockModule; > use Test::Deep qw( cmp_deeply ); >@@ -1257,6 +1257,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 { >@@ -1737,6 +1808,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; > >@@ -3157,6 +3290,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; > >@@ -3172,6 +3369,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 61318d5c27..d0506855b3 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; >@@ -99,7 +102,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 } ); >@@ -147,6 +150,33 @@ subtest 'transfer already at destination' => sub { > is( $dotransfer, 1, 'Transfer of reserved item succeeded 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 { >@@ -294,3 +324,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 7ad86b9783..e591abea11 100755 >--- a/t/db_dependent/Holds.t >+++ b/t/db_dependent/Holds.t >@@ -7,7 +7,7 @@ use t::lib::TestBuilder; > > use C4::Context; > >-use Test::More tests => 67; >+use Test::More tests => 68; > use MARC::Record; > > use C4::Biblio; >@@ -1326,5 +1326,32 @@ 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; > }; >diff --git a/t/db_dependent/Koha/Biblio.t b/t/db_dependent/Koha/Biblio.t >index b849fd3160..b2f0f1ff49 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 => 12; >+use Test::More tests => 13; > > use C4::Biblio; > use Koha::Database; >@@ -184,11 +184,11 @@ subtest 'pickup_locations' => sub { > > # Cleanup database > Koha::Holds->search->delete; >+ $dbh->do('DELETE FROM issues'); > Koha::Patrons->search->delete; > Koha::Items->search->delete; > Koha::Libraries->search->delete; > Koha::CirculationRules->search->delete; >- $dbh->do('DELETE FROM issues'); > Koha::CirculationRules->set_rules( > { > categorycode => undef, >@@ -549,3 +549,120 @@ subtest 'subscriptions() tests' => sub { > > $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 0309fa94f6..83c6f5e1f1 100755 >--- a/t/db_dependent/Koha/Item.t >+++ b/t/db_dependent/Koha/Item.t >@@ -19,7 +19,7 @@ > > use Modern::Perl; > >-use Test::More tests => 7; >+use Test::More tests => 8; > > use C4::Biblio; > use C4::Circulation; >@@ -516,3 +516,190 @@ subtest 'Tests for itemtype' => sub { > > $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; >+}; >diff --git a/t/db_dependent/Koha/Patron.t b/t/db_dependent/Koha/Patron.t >index ffec081dfc..59a3f76879 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 => 5; >+use Test::More tests => 6; > use Test::Exception; > > use Koha::Database; >@@ -241,3 +241,59 @@ subtest 'is_superlibrarian() tests' => sub { > > $schema->storage->txn_rollback; > }; >+ >+subtest 'recalls() tests' => sub { >+ >+ plan tests => 2; >+ >+ $schema->storage->txn_begin; >+ >+ my $patron = $builder->build_object({ class => 'Koha::Patrons' }); >+ 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 7e2e6f7b6c..724e55dbba 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 2d9e9db1b3..4153ae3b77 100755 >--- a/t/db_dependent/XSLT.t >+++ b/t/db_dependent/XSLT.t >@@ -34,7 +34,7 @@ my $builder = t::lib::TestBuilder->new; > $schema->storage->txn_begin; > > subtest 'buildKohaItemsNamespace status tests' => sub { >- plan tests => 13; >+ plan tests => 14; > my $itype = $builder->build_object({ class => 'Koha::ItemTypes' }); > my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes' }); > my $item = $builder->build_sample_item({ itype => $itype->itemtype }); >@@ -103,7 +103,8 @@ subtest 'buildKohaItemsNamespace status tests' => sub { > } > }); > $xml = C4::XSLT::buildKohaItemsNamespace( $item->biblionumber,[]); >- like($xml,qr{<status>Waiting</status>},"Waiting status takes precedence over In transit"); >+ like($xml,qr{<status>Waiting</status>},"Waiting status takes precedence over In transit (holds)"); >+ $hold->cancel; > > $builder->build({ source => "TmpHoldsqueue", value => { > itemnumber => $item->itemnumber >@@ -112,6 +113,15 @@ subtest 'buildKohaItemsNamespace status tests' => sub { > $xml = C4::XSLT::buildKohaItemsNamespace( $item->biblionumber,[]); > like($xml,qr{<status>Pending hold</status>},"Pending status takes precedence over all"); > >+ 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{<status>Waiting</status>},"Waiting status takes precedence over In transit (recalls)"); > > }; > >-- >2.11.0
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 19532
:
68569
|
68570
|
68571
|
68572
|
68642
|
68643
|
68644
|
68645
|
68653
|
69100
|
69102
|
69147
|
69148
|
69149
|
69170
|
69172
|
69221
|
69222
|
69223
|
69322
|
69353
|
69354
|
69405
|
69409
|
69410
|
69411
|
69841
|
69872
|
69873
|
69874
|
69875
|
69876
|
69877
|
69878
|
69879
|
69880
|
69881
|
69882
|
69883
|
69884
|
69885
|
69886
|
69887
|
69888
|
69889
|
69890
|
69986
|
70093
|
71409
|
71574
|
71575
|
71576
|
71577
|
71578
|
71579
|
71580
|
71581
|
71582
|
71583
|
71584
|
71585
|
71586
|
71587
|
71588
|
71589
|
71590
|
71591
|
71592
|
72244
|
72311
|
72312
|
72313
|
72314
|
72315
|
72316
|
72317
|
72318
|
72319
|
72320
|
72321
|
72322
|
72323
|
72324
|
72325
|
72326
|
72327
|
72328
|
72329
|
72330
|
72331
|
72332
|
72333
|
72334
|
72335
|
72336
|
72337
|
72338
|
72527
|
76817
|
76818
|
76819
|
76820
|
76821
|
76822
|
76823
|
76824
|
76825
|
76826
|
76827
|
76828
|
76829
|
76830
|
76831
|
76832
|
76833
|
76834
|
76835
|
76836
|
76837
|
76838
|
76839
|
76840
|
76841
|
76842
|
76843
|
76844
|
76845
|
76846
|
76847
|
77177
|
77178
|
77179
|
78755
|
78757
|
78759
|
78761
|
78763
|
78765
|
78766
|
78768
|
78770
|
78772
|
78773
|
78774
|
78775
|
78776
|
78777
|
78778
|
78780
|
78782
|
78785
|
78787
|
78789
|
78790
|
78792
|
78794
|
78797
|
78800
|
78801
|
78802
|
78803
|
78804
|
78805
|
78806
|
78807
|
78808
|
80763
|
80764
|
80765
|
80766
|
80767
|
80768
|
80769
|
80770
|
80771
|
80772
|
80773
|
80774
|
80775
|
80776
|
80777
|
80778
|
80779
|
80780
|
80781
|
80782
|
80783
|
80784
|
80785
|
80786
|
80787
|
80788
|
80789
|
80793
|
80878
|
81204
|
81205
|
81206
|
81207
|
81208
|
81209
|
81210
|
81211
|
81212
|
81213
|
81214
|
81215
|
81216
|
81217
|
81218
|
81219
|
81220
|
81221
|
81222
|
81223
|
81224
|
81225
|
81226
|
81227
|
81228
|
81229
|
81230
|
81231
|
81232
|
81233
|
81234
|
81235
|
81236
|
81237
|
81238
|
81239
|
81240
|
81241
|
81242
|
81243
|
81244
|
81245
|
81246
|
81247
|
81547
|
81548
|
81549
|
81550
|
81551
|
81552
|
81553
|
81554
|
81555
|
81556
|
81557
|
81558
|
81559
|
81560
|
81561
|
81562
|
81563
|
81564
|
81565
|
81566
|
81567
|
81568
|
81569
|
81583
|
81584
|
81585
|
81586
|
81587
|
81588
|
81595
|
81618
|
81652
|
81653
|
81654
|
81655
|
81656
|
81657
|
81658
|
81659
|
81660
|
81661
|
81662
|
81663
|
81664
|
81665
|
81666
|
81667
|
81668
|
81669
|
81670
|
81671
|
81672
|
81673
|
81674
|
81675
|
81676
|
81677
|
81678
|
81679
|
81680
|
81681
|
81682
|
81683
|
103887
|
103888
|
103890
|
103899
|
103900
|
103901
|
103999
|
104000
|
104001
|
104002
|
104005
|
104006
|
104007
|
104066
|
104067
|
104068
|
104069
|
104070
|
104071
|
104246
|
104247
|
104248
|
104249
|
104250
|
104322
|
104323
|
104324
|
104325
|
104326
|
104327
|
104328
|
104329
|
104330
|
104331
|
104332
|
104333
|
104334
|
104335
|
104336
|
104391
|
104392
|
104393
|
104394
|
104395
|
104396
|
104397
|
104398
|
104474
|
104475
|
104476
|
104477
|
104478
|
104479
|
104480
|
104481
|
104545
|
104546
|
104547
|
104548
|
104549
|
104550
|
104551
|
104552
|
104646
|
104647
|
104648
|
104649
|
104650
|
104651
|
104652
|
104653
|
104722
|
104723
|
104724
|
104725
|
104726
|
104727
|
104728
|
104729
|
104806
|
104807
|
104808
|
104809
|
104810
|
104811
|
104812
|
104813
|
104814
|
104815
|
104816
|
104817
|
104818
|
104819
|
104820
|
104821
|
104863
|
104864
|
104865
|
104866
|
104867
|
104868
|
104869
|
104870
|
104902
|
104903
|
104904
|
104905
|
104906
|
104907
|
104908
|
104909
|
104910
|
104913
|
104914
|
104915
|
104916
|
104917
|
104918
|
104919
|
104920
|
104988
|
104989
|
104990
|
104991
|
104992
|
106268
|
106269
|
106371
|
106372
|
106373
|
106374
|
106375
|
106376
|
106377
|
106378
|
106810
|
106811
|
106812
|
106813
|
106814
|
106815
|
106816
|
106817
|
106857
|
107198
|
107199
|
107200
|
107201
|
107202
|
107203
|
107204
|
107205
|
107807
|
107808
|
107809
|
107810
|
107811
|
107812
|
107813
|
107814
|
107918
|
107919
|
107920
|
107921
|
107922
|
107923
|
107924
|
107925
|
108222
|
108223
|
108224
|
108225
|
108226
|
108227
|
108228
|
108229
|
108980
|
108981
|
108982
|
109306
|
109307
|
109308
|
109309
|
109310
|
109311
|
109312
|
109313
|
109314
|
109315
|
109316
|
109317
|
109318
|
109463
|
109464
|
109465
|
109466
|
109467
|
109468
|
109469
|
109775
|
109776
|
109777
|
109778
|
109779
|
109780
|
109781
|
109782
|
109783
|
109784
|
109785
|
111199
|
111200
|
111201
|
111202
|
111203
|
111204
|
111205
|
111206
|
112316
|
112317
|
112321
|
112322
|
112323
|
112324
|
112325
|
112326
|
112346
|
112472
|
112491
|
112540
|
112612
|
112613
|
112614
|
112615
|
112616
|
112617
|
112618
|
112619
|
112620
|
112621
|
112622
|
113195
|
113196
|
113197
|
113198
|
113199
|
113200
|
113201
|
113202
|
113203
|
113204
|
113205
|
113286
|
113287
|
113288
|
113289
|
113290
|
113291
|
113292
|
113293
|
113294
|
113295
|
113296
|
113297
|
113298
|
113335
|
113351
|
113352
|
113353
|
113354
|
113355
|
113356
|
113357
|
113358
|
113359
|
113361
|
113362
|
113363
|
113364
|
113365
|
113430
|
113431
|
113432
|
113433
|
113434
|
113435
|
113436
|
113437
|
113438
|
113439
|
113440
|
113441
|
113442
|
113443
|
113444
|
113445
|
116121
|
116122
|
116123
|
116124
|
116125
|
116126
|
116127
|
116128
|
116129
|
116130
|
116131
|
116132
|
116133
|
116134
|
118267
|
118268
|
118269
|
118270
|
118271
|
118272
|
118273
|
118274
|
118275
|
118276
|
118277
|
118278
|
118279
|
118280
|
122993
|
122994
|
122995
|
122996
|
122997
|
122998
|
122999
|
123000
|
123001
|
123002
|
123003
|
123004
|
123005
|
126244
|
126245
|
126246
|
126247
|
126248
|
126249
|
126250
|
126251
|
126252
|
126253
|
126254
|
126255
|
126256
|
126257
|
126453
|
126454
|
126455
|
126456
|
126457
|
126458
|
126459
|
126460
|
126469
|
126470
|
126565
|
126566
|
126568
|
126569
|
126570
|
126571
|
126572
|
126573
|
126574
|
126575
|
126576
|
126577
|
126578
|
126579
|
126580
|
129305
|
129306
|
129307
|
129308
|
129309
|
129310
|
129311
|
129312
|
129313
|
129314
|
129315
|
129316
|
129317
|
129318
|
129319
|
130641
|
130642
|
130643
|
130644
|
130645
|
130646
|
130647
|
130648
|
130649
|
130650
|
130651
|
130652
|
130653
|
130654
|
130655
|
130656
|
131108
|
131156
|
131157
|
131158
|
131159
|
131160
|
131193
|
131213
|
131257
|
131260
|
131261
|
131262
|
131263
|
131264
|
131265
|
131266
|
131267
|
131268
|
131269
|
131270
|
131271
|
131272
|
131273
|
131274
|
131275
|
131276
|
131277
|
131278
|
131279
|
131280
|
131281
|
131282
|
131330
|
131629
|
131630
|
131631
|
131664
|
131665
|
131666
|
131701
|
132332