View | Details | Raw Unified | Return to bug 19532
Collapse All | Expand All

(-)a/C4/Circulation.pm (-2 / +99 lines)
Lines 299-304 The item was reserved. The value is a reference-to-hash whose keys are fields fr Link Here
299
299
300
The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored.
300
The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored.
301
301
302
=item C<RecallPlacedAtHoldingBranch>
303
304
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.
305
306
=item C<RecallFound>
307
308
A recall for this item was found, and the item needs to be transferred to the recall's pickup branch.
309
302
=back
310
=back
303
311
304
=back
312
=back
Lines 360-365 sub transferbook { Link Here
360
        $dotransfer = 1;
368
        $dotransfer = 1;
361
    }
369
    }
362
370
371
    # find recall
372
    my $recall = Koha::Recalls->find({ itemnumber => $itemnumber, status => 'T' });
373
    if ( defined $recall ) {
374
        # do a transfer if the recall branch is different to the item holding branch
375
        if ( $recall->branchcode eq $fbr ) {
376
            $dotransfer = 0;
377
            $messages->{'RecallPlacedAtHoldingBranch'} = 1;
378
        } else {
379
            $dotransfer = 1;
380
            $messages->{'RecallFound'} = $recall;
381
        }
382
    }
383
363
    #actually do the transfer....
384
    #actually do the transfer....
364
    if ($dotransfer) {
385
    if ($dotransfer) {
365
        ModItemTransfer( $itemnumber, $fbr, $tbr, $trigger );
386
        ModItemTransfer( $itemnumber, $fbr, $tbr, $trigger );
Lines 669-674 sticky due date is invalid or due date in the past Link Here
669
690
670
if the borrower borrows to much things
691
if the borrower borrows to much things
671
692
693
=head3 RECALLED
694
695
recalled by someone else
696
672
=cut
697
=cut
673
698
674
sub CanBookBeIssued {
699
sub CanBookBeIssued {
Lines 1020-1026 sub CanBookBeIssued { Link Here
1020
        }
1045
        }
1021
    }
1046
    }
1022
1047
1023
    unless ( $ignore_reserves ) {
1048
    my $recall;
1049
    # CHECK IF ITEM HAS BEEN RECALLED BY ANOTHER PATRON
1050
    # Only bother doing this if UseRecalls is enabled and the item is recallable
1051
    # Don't look at recalls that are in transit
1052
    if ( C4::Context->preference('UseRecalls') and $item_object->can_be_recalled({ patron => $patron }) ) {
1053
        my @recalls = $item_object->biblio->recalls;
1054
1055
        foreach my $r ( @recalls ) {
1056
            if ( $r->item_level_recall and
1057
                 $r->itemnumber == $item_object->itemnumber and
1058
                 $r->borrowernumber != $patron->borrowernumber and
1059
                 !$r->in_transit ) {
1060
                # this specific item has been recalled by a different patron
1061
                $needsconfirmation{RECALLED} = $r;
1062
                $recall = $r;
1063
                last;
1064
            }
1065
            elsif ( !$r->item_level_recall and
1066
                    $r->borrowernumber != $patron->borrowernumber and
1067
                    !$r->in_transit ) {
1068
                # a different patron has placed a biblio-level recall and this item is eligible to fill it
1069
                $needsconfirmation{RECALLED} = $r;
1070
                $recall = $r;
1071
                last;
1072
            }
1073
        }
1074
    }
1075
1076
    unless ( $ignore_reserves and defined $recall ) {
1024
        # See if the item is on reserve.
1077
        # See if the item is on reserve.
1025
        my ( $restype, $res ) = C4::Reserves::CheckReserves( $item_object->itemnumber );
1078
        my ( $restype, $res ) = C4::Reserves::CheckReserves( $item_object->itemnumber );
1026
        if ($restype) {
1079
        if ($restype) {
Lines 1290-1295 AddIssue does the following things : Link Here
1290
          * RESERVE PLACED ?
1343
          * RESERVE PLACED ?
1291
              - fill reserve if reserve to this patron
1344
              - fill reserve if reserve to this patron
1292
              - cancel reserve or not, otherwise
1345
              - cancel reserve or not, otherwise
1346
          * RECALL PLACED ?
1347
              - fill recall if recall to this patron
1348
              - cancel recall or not
1349
              - revert recall's waiting status or not
1293
          * TRANSFERT PENDING ?
1350
          * TRANSFERT PENDING ?
1294
              - complete the transfert
1351
              - complete the transfert
1295
          * ISSUE THE BOOK
1352
          * ISSUE THE BOOK
Lines 1304-1309 sub AddIssue { Link Here
1304
    my $onsite_checkout = $params && $params->{onsite_checkout} ? 1 : 0;
1361
    my $onsite_checkout = $params && $params->{onsite_checkout} ? 1 : 0;
1305
    my $switch_onsite_checkout = $params && $params->{switch_onsite_checkout};
1362
    my $switch_onsite_checkout = $params && $params->{switch_onsite_checkout};
1306
    my $auto_renew = $params && $params->{auto_renew};
1363
    my $auto_renew = $params && $params->{auto_renew};
1364
    my $cancel_recall = $params && $params->{cancel_recall};
1365
    my $recall_id = $params && $params->{recall_id};
1307
    my $dbh          = C4::Context->dbh;
1366
    my $dbh          = C4::Context->dbh;
1308
    my $barcodecheck = CheckValidBarcode($barcode);
1367
    my $barcodecheck = CheckValidBarcode($barcode);
1309
1368
Lines 1375-1380 sub AddIssue { Link Here
1375
                AddReturn( $item_object->barcode, C4::Context->userenv->{'branch'} );
1434
                AddReturn( $item_object->barcode, C4::Context->userenv->{'branch'} );
1376
            }
1435
            }
1377
1436
1437
            Koha::Recalls->move_recall({ action => $cancel_recall, recall_id => $recall_id, itemnumber => $item_object->itemnumber, borrowernumber => $borrower->{borrowernumber} });
1438
1378
            C4::Reserves::MoveReserve( $item_object->itemnumber, $borrower->{'borrowernumber'}, $cancelreserve );
1439
            C4::Reserves::MoveReserve( $item_object->itemnumber, $borrower->{'borrowernumber'}, $cancelreserve );
1379
1440
1380
            # Starting process for transfer job (checking transfert and validate it if we have one)
1441
            # Starting process for transfer job (checking transfert and validate it if we have one)
Lines 1836-1841 Value 1 if return is successful. Link Here
1836
1897
1837
If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer.
1898
If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer.
1838
1899
1900
=item C<RecallFound>
1901
1902
This item can fill a recall. The recall object is returned. If the recall pickup branch differs from
1903
the branch this item is being returned at, C<RecallNeedsTransfer> is also returned which contains this
1904
branchcode.
1905
1906
=item C<TransferredRecall>
1907
1908
This item has been transferred to this branch to fill a recall. The recall object is returned.
1909
1839
=back
1910
=back
1840
1911
1841
C<$iteminformation> is a reference-to-hash, giving information about the
1912
C<$iteminformation> is a reference-to-hash, giving information about the
Lines 2076-2081 sub AddReturn { Link Here
2076
        }
2147
        }
2077
    }
2148
    }
2078
2149
2150
    # find recalls...
2151
    # check if this item is recallable first, which includes checking if UseRecalls syspref is enabled
2152
    my $recall = $item->check_recalls if $item->can_be_waiting_recall;
2153
    if ( defined $recall ) {
2154
        $messages->{RecallFound} = $recall;
2155
        if ( $recall->branchcode ne $branch ) {
2156
            $messages->{RecallNeedsTransfer} = $branch;
2157
        }
2158
    }
2159
2079
    # find reserves.....
2160
    # find reserves.....
2080
    # launch the Checkreserves routine to find any holds
2161
    # launch the Checkreserves routine to find any holds
2081
    my ($resfound, $resrec);
2162
    my ($resfound, $resrec);
Lines 2143-2150 sub AddReturn { Link Here
2143
        $request->status('RET') if $request;
2224
        $request->status('RET') if $request;
2144
    }
2225
    }
2145
2226
2227
    my $transfer_recall = Koha::Recalls->find({ itemnumber => $item->itemnumber, status => 'T' }); # all recalls that have triggered a transfer will have an allocated itemnumber
2228
    if ( $transfer_recall && $transfer_recall->branchcode eq $branch ) {
2229
        $messages->{TransferredRecall} = $transfer_recall;
2230
    }
2231
2146
    # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
2232
    # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
2147
    if (!$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) and not $messages->{'WrongTransfer'}){
2233
    if (!$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) and not $messages->{'WrongTransfer'} and not $messages->{TransferredRecall} and $recall->branchcode ne $branch){
2148
        my $BranchTransferLimitsType = C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ? 'effective_itemtype' : 'ccode';
2234
        my $BranchTransferLimitsType = C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ? 'effective_itemtype' : 'ccode';
2149
        if  (C4::Context->preference("AutomaticItemReturn"    ) or
2235
        if  (C4::Context->preference("AutomaticItemReturn"    ) or
2150
            (C4::Context->preference("UseBranchTransferLimits") and
2236
            (C4::Context->preference("UseBranchTransferLimits") and
Lines 2835-2840 sub CanBookBeRenewed { Link Here
2835
        }
2921
        }
2836
    }
2922
    }
2837
2923
2924
    my $recall = $item->check_recalls if $item->can_be_waiting_recall;
2925
    if ( defined $recall ) {
2926
        if ( $recall->item_level_recall ) {
2927
            # item-level recall. check if this item is the recalled item, otherwise renewal will be allowed
2928
            return ( 0, 'recalled' ) if ( $recall->itemnumber == $item->itemnumber );
2929
        } else {
2930
            # biblio-level recall, so only disallow renewal if the biblio-level recall has been fulfilled by a different item
2931
            return ( 0, 'recalled' ) unless ( $recall->waiting );
2932
        }
2933
    }
2934
2838
    my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2935
    my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2839
2936
2840
    # This item can fill one or more unfilled reserve, can those unfilled reserves
2937
    # This item can fill one or more unfilled reserve, can those unfilled reserves
(-)a/C4/Search.pm (+9 lines)
Lines 1893-1898 sub searchResults { Link Here
1893
        my $can_place_holds       = 0;
1893
        my $can_place_holds       = 0;
1894
        my $item_onhold_count     = 0;
1894
        my $item_onhold_count     = 0;
1895
        my $notforloan_count      = 0;
1895
        my $notforloan_count      = 0;
1896
        my $item_recalled_count   = 0;
1896
        my $items_count           = scalar(@fields);
1897
        my $items_count           = scalar(@fields);
1897
        my $maxitems_pref = C4::Context->preference('maxItemsinSearchResults');
1898
        my $maxitems_pref = C4::Context->preference('maxItemsinSearchResults');
1898
        my $maxitems = $maxitems_pref ? $maxitems_pref - 1 : 1;
1899
        my $maxitems = $maxitems_pref ? $maxitems_pref - 1 : 1;
Lines 1987-1992 sub searchResults { Link Here
1987
                # is item on the reserve shelf?
1988
                # is item on the reserve shelf?
1988
                my $reservestatus = '';
1989
                my $reservestatus = '';
1989
1990
1991
                # is item a waiting recall?
1992
                my $recallstatus = '';
1993
1990
                unless ($item->{withdrawn}
1994
                unless ($item->{withdrawn}
1991
                        || $item->{itemlost}
1995
                        || $item->{itemlost}
1992
                        || $item->{damaged}
1996
                        || $item->{damaged}
Lines 2008-2013 sub searchResults { Link Here
2008
                    #
2012
                    #
2009
                    ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
2013
                    ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
2010
                    $reservestatus = C4::Reserves::GetReserveStatus( $item->{itemnumber} );
2014
                    $reservestatus = C4::Reserves::GetReserveStatus( $item->{itemnumber} );
2015
                    $recallstatus = 'Waiting' if Koha::Recalls->search({ itemnumber => $item->{itemnumber}, status => 'W' })->count;
2011
                }
2016
                }
2012
2017
2013
                # item is withdrawn, lost, damaged, not for loan, reserved or in transit
2018
                # item is withdrawn, lost, damaged, not for loan, reserved or in transit
Lines 2016-2021 sub searchResults { Link Here
2016
                    || $item->{damaged}
2021
                    || $item->{damaged}
2017
                    || $item->{notforloan}
2022
                    || $item->{notforloan}
2018
                    || $reservestatus eq 'Waiting'
2023
                    || $reservestatus eq 'Waiting'
2024
                    || $recallstatus eq 'Waiting'
2019
                    || ($transfertwhen && $transfertwhen ne ''))
2025
                    || ($transfertwhen && $transfertwhen ne ''))
2020
                {
2026
                {
2021
                    $withdrawn_count++        if $item->{withdrawn};
2027
                    $withdrawn_count++        if $item->{withdrawn};
Lines 2023-2028 sub searchResults { Link Here
2023
                    $itemdamaged_count++     if $item->{damaged};
2029
                    $itemdamaged_count++     if $item->{damaged};
2024
                    $item_in_transit_count++ if $transfertwhen && $transfertwhen ne '';
2030
                    $item_in_transit_count++ if $transfertwhen && $transfertwhen ne '';
2025
                    $item_onhold_count++     if $reservestatus eq 'Waiting';
2031
                    $item_onhold_count++     if $reservestatus eq 'Waiting';
2032
                    $item_recalled_count++   if $recallstatus eq 'Waiting';
2026
                    $item->{status} = ($item->{withdrawn}//q{}) . "-" . ($item->{itemlost}//q{}) . "-" . ($item->{damaged}//q{}) . "-" . ($item->{notforloan}//q{});
2033
                    $item->{status} = ($item->{withdrawn}//q{}) . "-" . ($item->{itemlost}//q{}) . "-" . ($item->{damaged}//q{}) . "-" . ($item->{notforloan}//q{});
2027
2034
2028
                    # can place a hold on a item if
2035
                    # can place a hold on a item if
Lines 2044-2049 sub searchResults { Link Here
2044
                        $other_items->{$key}->{$_} = $item->{$_};
2051
                        $other_items->{$key}->{$_} = $item->{$_};
2045
                    }
2052
                    }
2046
                    $other_items->{$key}->{intransit} = ( $transfertwhen ne '' ) ? 1 : 0;
2053
                    $other_items->{$key}->{intransit} = ( $transfertwhen ne '' ) ? 1 : 0;
2054
                    $other_items->{$key}->{recalled} = ($recallstatus) ? 1 : 0;
2047
                    $other_items->{$key}->{onhold} = ($reservestatus) ? 1 : 0;
2055
                    $other_items->{$key}->{onhold} = ($reservestatus) ? 1 : 0;
2048
                    $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value and $item->{notforloan};
2056
                    $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value and $item->{notforloan};
2049
                    $other_items->{$key}->{count}++ if $item->{$hbranch};
2057
                    $other_items->{$key}->{count}++ if $item->{$hbranch};
Lines 2112-2117 sub searchResults { Link Here
2112
        $oldbiblio->{damagedcount}         = $itemdamaged_count;
2120
        $oldbiblio->{damagedcount}         = $itemdamaged_count;
2113
        $oldbiblio->{intransitcount}       = $item_in_transit_count;
2121
        $oldbiblio->{intransitcount}       = $item_in_transit_count;
2114
        $oldbiblio->{onholdcount}          = $item_onhold_count;
2122
        $oldbiblio->{onholdcount}          = $item_onhold_count;
2123
        $oldbiblio->{recalledcount}        = $item_recalled_count;
2115
        $oldbiblio->{orderedcount}         = $ordered_count;
2124
        $oldbiblio->{orderedcount}         = $ordered_count;
2116
        $oldbiblio->{notforloancount}      = $notforloan_count;
2125
        $oldbiblio->{notforloancount}      = $notforloan_count;
2117
2126
(-)a/C4/XSLT.pm (-1 / +3 lines)
Lines 321-330 sub buildKohaItemsNamespace { Link Here
321
        my $status;
321
        my $status;
322
        my $substatus = '';
322
        my $substatus = '';
323
323
324
        my $recalls = Koha::Recalls->search({ itemnumber => $item->itemnumber, status => 'W' });
325
324
        if ($item->has_pending_hold) {
326
        if ($item->has_pending_hold) {
325
            $status = 'Pending hold';
327
            $status = 'Pending hold';
326
        }
328
        }
327
        elsif ( $item->holds->waiting->count ) {
329
        elsif ( $item->holds->waiting->count || $recalls->count ) {
328
            $status = 'Waiting';
330
            $status = 'Waiting';
329
        }
331
        }
330
        elsif ($item->get_transfer) {
332
        elsif ($item->get_transfer) {
(-)a/Koha/Biblio.pm (+102 lines)
Lines 815-820 sub to_api_mapping { Link Here
815
    };
815
    };
816
}
816
}
817
817
818
=head3 recalls
819
820
    my $recalls = $biblio->recalls;
821
822
Return all active recalls attached to this biblio, sorted by oldest first
823
824
=cut
825
826
sub recalls {
827
    my ( $self ) = @_;
828
    $self->{_recalls} ||= Koha::Recalls->search({ biblionumber => $self->biblionumber, old => undef }, { order_by => { -asc => 'recalldate' } } );
829
    return $self->{_recalls};
830
}
831
832
=head3 can_be_recalled
833
834
    my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
835
836
Does biblio-level checks and returns the items attached to this biblio that are available for recall
837
838
=cut
839
840
sub can_be_recalled {
841
    my ( $self, $params ) = @_;
842
843
    return 0 if !( C4::Context->preference('UseRecalls') );
844
845
    my $patron = $params->{patron};
846
847
    my $branchcode = C4::Context->userenv->{'branch'};
848
    if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
849
        $branchcode = $patron->branchcode;
850
    }
851
852
    # items are unavailable for recall if they are lost, withdrawn or notforloan
853
    my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 });
854
855
    # if there are no available items at all, no recall can be placed
856
    return 0 if ( scalar @items == 0 );
857
858
    my @itemtypes;
859
    my @itemnumbers;
860
    foreach my $item ( @items ) {
861
        push( @itemtypes, $item->effective_itemtype );
862
        push( @itemnumbers, $item->itemnumber );
863
    }
864
865
    # Check the circulation rule for each relevant itemtype for this biblio
866
    my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
867
    foreach my $itemtype ( @itemtypes ) {
868
        my $rule = Koha::CirculationRules->get_effective_rules({
869
            branchcode => $branchcode,
870
            categorycode => $patron ? $patron->categorycode : undef,
871
            itemtype => $itemtype,
872
            rules => [
873
                'recalls_allowed',
874
                'recalls_per_record',
875
                'on_shelf_recalls',
876
            ],
877
        });
878
        push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
879
        push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
880
        push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
881
    }
882
    my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
883
    my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
884
    my %on_shelf_recalls_count = ();
885
    foreach my $count ( @on_shelf_recalls ) {
886
        $on_shelf_recalls_count{$count}++;
887
    }
888
    my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
889
890
    # check recalls allowed has been set and is not zero
891
    return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
892
893
    if ( $patron ) {
894
        # check borrower has not reached open recalls allowed limit
895
        return 0 if ( $patron->recalls->count >= $recalls_allowed );
896
897
        # check borrower has not reached open recalls allowed per record limit
898
        return 0 if ( $patron->recalls({ biblionumber => $self->biblionumber })->count >= $recalls_per_record );
899
900
        # check if any of the items under this biblio are already checked out by this borrower
901
        return 0 if ( Koha::Checkouts->search({ itemnumber => [ @itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
902
    }
903
904
    # check item availability
905
    my $checked_out_count = 0;
906
    foreach (@items) {
907
        if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
908
    }
909
910
    # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
911
    return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
912
913
    # can't recall if no items have been checked out
914
    return 0 if ( $checked_out_count == 0 );
915
916
    # can recall
917
    return @items;
918
}
919
818
=head2 Internal methods
920
=head2 Internal methods
819
921
820
=head3 type
922
=head3 type
(-)a/Koha/Item.pm (+181 lines)
Lines 842-847 sub _after_item_action_hooks { Link Here
842
    }
842
    }
843
}
843
}
844
844
845
=head3 recall
846
847
    my $recall = $item->recall;
848
849
Return the relevant recall for this item
850
851
=cut
852
853
sub recall {
854
    my ( $self ) = @_;
855
    my @recalls = Koha::Recalls->search({ biblionumber => $self->biblionumber, old => undef }, { order_by => { -asc => 'recalldate' } });
856
    foreach my $recall (@recalls) {
857
        if ( $recall->item_level_recall and $recall->itemnumber == $self->itemnumber ){
858
            return $recall;
859
        }
860
    }
861
    # no item-level recall to return, so return earliest biblio-level
862
    # FIXME: eventually this will be based on priority
863
    return $recalls[0];
864
}
865
866
=head3 can_be_recalled
867
868
    if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
869
870
Does item-level checks and returns if items can be recalled by this borrower
871
872
=cut
873
874
sub can_be_recalled {
875
    my ( $self, $params ) = @_;
876
877
    return 0 if !( C4::Context->preference('UseRecalls') );
878
879
    # check if this item is not for loan, withdrawn or lost
880
    return 0 if ( $self->notforloan != 0 );
881
    return 0 if ( $self->itemlost != 0 );
882
    return 0 if ( $self->withdrawn != 0 );
883
884
    # check if this item is not checked out - if not checked out, can't be recalled
885
    return 0 if ( !defined( $self->checkout ) );
886
887
    my $patron = $params->{patron};
888
889
    my $branchcode = C4::Context->userenv->{'branch'};
890
    if ( $patron ) {
891
        $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
892
    }
893
894
    # Check the circulation rule for each relevant itemtype for this item
895
    my $rule = Koha::CirculationRules->get_effective_rules({
896
        branchcode => $branchcode,
897
        categorycode => $patron ? $patron->categorycode : undef,
898
        itemtype => $self->effective_itemtype,
899
        rules => [
900
            'recalls_allowed',
901
            'recalls_per_record',
902
            'on_shelf_recalls',
903
        ],
904
    });
905
906
    # check recalls allowed has been set and is not zero
907
    return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
908
909
    if ( $patron ) {
910
        # check borrower has not reached open recalls allowed limit
911
        return 0 if ( $patron->recalls->count >= $rule->{recalls_allowed} );
912
913
        # check borrower has not reach open recalls allowed per record limit
914
        return 0 if ( $patron->recalls({ biblionumber => $self->biblionumber })->count >= $rule->{recalls_per_record} );
915
916
        # check if this patron has already recalled this item
917
        return 0 if ( Koha::Recalls->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber, old => undef })->count > 0 );
918
919
        # check if this patron has already checked out this item
920
        return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
921
922
        # check if this patron has already reserved this item
923
        return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
924
    }
925
926
    # check item availability
927
    # items are unavailable for recall if they are lost, withdrawn or notforloan
928
    my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 });
929
930
    # if there are no available items at all, no recall can be placed
931
    return 0 if ( scalar @items == 0 );
932
933
    my $checked_out_count = 0;
934
    foreach (@items) {
935
        if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
936
    }
937
938
    # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
939
    return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
940
941
    # can't recall if no items have been checked out
942
    return 0 if ( $checked_out_count == 0 );
943
944
    # can recall
945
    return 1;
946
}
947
948
=head3 can_be_waiting_recall
949
950
    if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
951
952
Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
953
At this point the item has already been recalled. We are now at the checkin and set waiting stage.
954
955
=cut
956
957
sub can_be_waiting_recall {
958
    my ( $self ) = @_;
959
960
    return 0 if !( C4::Context->preference('UseRecalls') );
961
962
    # check if this item is not for loan, withdrawn or lost
963
    return 0 if ( $self->notforloan != 0 );
964
    return 0 if ( $self->itemlost != 0 );
965
    return 0 if ( $self->withdrawn != 0 );
966
967
    my $branchcode = $self->holdingbranch;
968
    if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
969
        $branchcode = C4::Context->userenv->{'branch'};
970
    } else {
971
        $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
972
    }
973
974
    # Check the circulation rule for each relevant itemtype for this item
975
    my $rule = Koha::CirculationRules->get_effective_rules({
976
        branchcode => $branchcode,
977
        categorycode => undef,
978
        itemtype => $self->effective_itemtype,
979
        rules => [
980
            'recalls_allowed',
981
        ],
982
    });
983
984
    # check recalls allowed has been set and is not zero
985
    return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
986
987
    # can recall
988
    return 1;
989
}
990
991
=head3 check_recalls
992
993
    my $recall = $item->check_recalls;
994
995
Get the most relevant recall for this item.
996
997
=cut
998
999
sub check_recalls {
1000
    my ( $self ) = @_;
1001
1002
    my @recalls = Koha::Recalls->search({ itemnumber => $self->itemnumber, status => [ 'R','O','W' ] }, { order_by => { -asc => 'recalldate' } });
1003
    if ( scalar @recalls == 0 ) {
1004
        # no item-level recalls or recalls waiting for this specific item
1005
        # look for biblio-level recalls
1006
        @recalls = Koha::Recalls->search({ biblionumber => $self->biblionumber, itemnumber => [ $self->itemnumber, undef ], status => [ 'R','O','W' ] }, { order_by => { -asc => 'recalldate' } });
1007
    }
1008
1009
    my $recall;
1010
    # iterate through relevant recalls to find the best one.
1011
    # if we come across a waiting recall, use this one.
1012
    # 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.
1013
    foreach my $r ( @recalls ) {
1014
        if ( $r->waiting ) {
1015
            $recall = $r;
1016
            last;
1017
        }
1018
    }
1019
    unless ( defined $recall ) {
1020
        $recall = $recalls[0];
1021
    }
1022
1023
    return $recall;
1024
}
1025
845
=head3 _type
1026
=head3 _type
846
1027
847
=cut
1028
=cut
(-)a/Koha/Patron.pm (+24 lines)
Lines 1712-1717 sub to_api_mapping { Link Here
1712
    };
1712
    };
1713
}
1713
}
1714
1714
1715
=head3 recalls
1716
1717
    my $recalls = $patron->recalls;
1718
1719
    my $recalls = $patron->recalls({ biblionumber => $biblionumber });
1720
1721
Return the patron's active recalls - total, or on a specific biblio
1722
1723
=cut
1724
1725
sub recalls {
1726
    my ( $self, $params ) = @_;
1727
1728
    my $biblionumber = $params->{biblionumber};
1729
1730
    my $recalls_rs = Koha::Recalls->search({ borrowernumber => $self->borrowernumber, old => undef });
1731
1732
    if ( $biblionumber ) {
1733
        $recalls_rs = Koha::Recalls->search({ borrowernumber => $self->borrowernumber, old => undef, biblionumber => $biblionumber });
1734
    }
1735
1736
    return $recalls_rs;
1737
}
1738
1715
=head2 Internal methods
1739
=head2 Internal methods
1716
1740
1717
=head3 _type
1741
=head3 _type
(-)a/Koha/Template/Plugin/Biblio.pm (+9 lines)
Lines 27-32 use Koha::Biblios; Link Here
27
use Koha::Patrons;
27
use Koha::Patrons;
28
use Koha::ArticleRequests;
28
use Koha::ArticleRequests;
29
use Koha::ArticleRequest::Status;
29
use Koha::ArticleRequest::Status;
30
use Koha::Recalls;
30
31
31
sub HoldsCount {
32
sub HoldsCount {
32
    my ( $self, $biblionumber ) = @_;
33
    my ( $self, $biblionumber ) = @_;
Lines 63-66 sub CanArticleRequest { Link Here
63
    return $biblio ? $biblio->can_article_request( $borrower ) : 0;
64
    return $biblio ? $biblio->can_article_request( $borrower ) : 0;
64
}
65
}
65
66
67
sub RecallsCount {
68
    my ( $self, $biblionumber ) = @_;
69
70
    my $recalls = Koha::Recalls->search({ biblionumber => $biblionumber, old => undef });
71
72
    return $recalls->count;
73
}
74
66
1;
75
1;
(-)a/t/db_dependent/Koha/Biblio.t (-1 / +117 lines)
Lines 17-23 Link Here
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::More tests => 12;
20
use Test::More tests => 13;
21
21
22
use C4::Biblio;
22
use C4::Biblio;
23
use Koha::Database;
23
use Koha::Database;
Lines 569-571 subtest 'subscriptions() tests' => sub { Link Here
569
569
570
    $schema->storage->txn_rollback;
570
    $schema->storage->txn_rollback;
571
};
571
};
572
573
subtest 'Recalls tests' => sub {
574
575
    plan tests => 12;
576
577
    $schema->storage->txn_begin;
578
    my $item1 = $builder->build_sample_item;
579
    my $biblio = $item1->biblio;
580
    my $branchcode = $item1->holdingbranch;
581
    my $patron1 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } });
582
    my $patron2 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } });
583
    my $patron3 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } });
584
    my $item2 = $builder->build_object({ class => 'Koha::Items', value => { holdingbranch => $branchcode, homebranch => $branchcode, biblionumber => $biblio->biblionumber, itype => $item1->effective_itemtype } });
585
    t::lib::Mocks::mock_userenv({ patron => $patron1 });
586
587
    my $recall1 = Koha::Recall->new({
588
        borrowernumber => $patron1->borrowernumber,
589
        recalldate => Koha::DateUtils::dt_from_string,
590
        biblionumber => $biblio->biblionumber,
591
        branchcode => $branchcode,
592
        status => 'R',
593
        itemnumber => $item1->itemnumber,
594
        expirationdate => undef,
595
        item_level_recall => 1
596
    })->store;
597
    my $recall2 = Koha::Recall->new({
598
        borrowernumber => $patron2->borrowernumber,
599
        recalldate => Koha::DateUtils::dt_from_string,
600
        biblionumber => $biblio->biblionumber,
601
        branchcode => $branchcode,
602
        status => 'R',
603
        itemnumber => undef,
604
        expirationdate => undef,
605
        item_level_recall => 0
606
    })->store;
607
    my $recall3 = Koha::Recall->new({
608
        borrowernumber => $patron3->borrowernumber,
609
        recalldate => Koha::DateUtils::dt_from_string,
610
        biblionumber => $biblio->biblionumber,
611
        branchcode => $branchcode,
612
        status => 'R',
613
        itemnumber => $item1->itemnumber,
614
        expirationdate => undef,
615
        item_level_recall => 1
616
    })->store;
617
618
    my $recalls_count = $biblio->recalls->count;
619
    is( $recalls_count, 3, 'Correctly get number of active recalls for biblio' );
620
621
    $recall1->set_cancelled;
622
    $recall2->set_expired({ interface => 'COMMANDLINE' });
623
624
    $recalls_count = $biblio->recalls->count;
625
    is( $recalls_count, 1, 'Correctly get number of active recalls for biblio' );
626
627
    t::lib::Mocks::mock_preference('UseRecalls', 0);
628
    is( $biblio->can_be_recalled({ patron => $patron1 }), 0, "Can't recall with UseRecalls disabled" );
629
630
    t::lib::Mocks::mock_preference("UseRecalls", 1);
631
    $item1->update({ notforloan => 1 });
632
    is( $biblio->can_be_recalled({ patron => $patron1 }), 0, "Can't recall with no available items" );
633
634
    $item1->update({ notforloan => 0 });
635
    Koha::CirculationRules->set_rules({
636
        branchcode => $branchcode,
637
        categorycode => $patron1->categorycode,
638
        itemtype => $item1->effective_itemtype,
639
        rules => {
640
            recalls_allowed => 0,
641
            recalls_per_record => 1,
642
            on_shelf_recalls => 'all',
643
        },
644
    });
645
    is( $biblio->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if recalls_allowed = 0" );
646
647
    Koha::CirculationRules->set_rules({
648
        branchcode => $branchcode,
649
        categorycode => $patron1->categorycode,
650
        itemtype => $item1->effective_itemtype,
651
        rules => {
652
            recalls_allowed => 1,
653
            recalls_per_record => 1,
654
            on_shelf_recalls => 'all',
655
        },
656
    });
657
    is( $biblio->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has more existing recall(s) than recalls_allowed" );
658
    is( $biblio->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has more existing recall(s) than recalls_per_record" );
659
660
    $recall1->set_cancelled;
661
    C4::Circulation::AddIssue( $patron1->unblessed, $item2->barcode );
662
    is( $biblio->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has already checked out an item attached to this biblio" );
663
664
    is( $biblio->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if on_shelf_recalls = all and items are still available" );
665
666
    Koha::CirculationRules->set_rules({
667
        branchcode => $branchcode,
668
        categorycode => $patron1->categorycode,
669
        itemtype => $item1->effective_itemtype,
670
        rules => {
671
            recalls_allowed => 1,
672
            recalls_per_record => 1,
673
            on_shelf_recalls => 'any',
674
        },
675
    });
676
    C4::Circulation::AddReturn( $item2->barcode, $branchcode );
677
    is( $biblio->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if no items are checked out" );
678
679
    $recall2->set_cancelled;
680
    C4::Circulation::AddIssue( $patron2->unblessed, $item2->barcode );
681
    is( $biblio->can_be_recalled({ patron => $patron1 }), 2, "Can recall two items" );
682
683
    $item1->update({ withdrawn => 1 });
684
    is( $biblio->can_be_recalled({ patron => $patron1 }), 1, "Can recall one item" );
685
686
    $schema->storage->txn_rollback;
687
};
(-)a/t/db_dependent/Koha/Item.t (-2 / +146 lines)
Lines 19-25 Link Here
19
19
20
use Modern::Perl;
20
use Modern::Perl;
21
21
22
use Test::More tests => 6;
22
use Test::More tests => 7;
23
23
24
use C4::Biblio;
24
use C4::Biblio;
25
use C4::Circulation;
25
use C4::Circulation;
Lines 493-495 subtest 'renewal_branchcode' => sub { Link Here
493
    is( $item->renewal_branchcode({branch=>'MANATEE'}), $item->homebranch, "If interface opac and OpacRenewalBranch set to itemhomebranch, we get homebranch of item even if branch passed");
493
    is( $item->renewal_branchcode({branch=>'MANATEE'}), $item->homebranch, "If interface opac and OpacRenewalBranch set to itemhomebranch, we get homebranch of item even if branch passed");
494
494
495
};
495
};
496
- 
496
497
subtest 'Recalls tests' => sub {
498
499
    plan tests => 17;
500
501
    $schema->storage->txn_begin;
502
503
    my $item1 = $builder->build_sample_item;
504
    my $biblio = $item1->biblio;
505
    my $branchcode = $item1->holdingbranch;
506
    my $patron1 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } });
507
    my $patron2 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } });
508
    my $patron3 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } });
509
    my $item2 = $builder->build_object({ class => 'Koha::Items', value => { holdingbranch => $branchcode, homebranch => $branchcode, biblionumber => $biblio->biblionumber, itype => $item1->effective_itemtype } });
510
    t::lib::Mocks::mock_userenv({ patron => $patron1 });
511
512
    my $recall1 = Koha::Recall->new({
513
        borrowernumber => $patron1->borrowernumber,
514
        recalldate => Koha::DateUtils::dt_from_string,
515
        biblionumber => $biblio->biblionumber,
516
        branchcode => $branchcode,
517
        status => 'R',
518
        itemnumber => $item1->itemnumber,
519
        expirationdate => undef,
520
        item_level_recall => 1
521
    })->store;
522
    my $recall2 = Koha::Recall->new({
523
        borrowernumber => $patron2->borrowernumber,
524
        recalldate => Koha::DateUtils::dt_from_string,
525
        biblionumber => $biblio->biblionumber,
526
        branchcode => $branchcode,
527
        status => 'R',
528
        itemnumber => $item1->itemnumber,
529
        expirationdate => undef,
530
        item_level_recall =>1
531
    })->store;
532
533
    is( $item1->recall->borrowernumber, $patron1->borrowernumber, 'Correctly returns most relevant recall' );
534
535
    $recall2->set_cancelled;
536
537
    t::lib::Mocks::mock_preference('UseRecalls', 0);
538
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall with UseRecalls disabled" );
539
540
    t::lib::Mocks::mock_preference("UseRecalls", 1);
541
542
    $item1->update({ notforloan => 1 });
543
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall that is not for loan" );
544
    $item1->update({ notforloan => 0, itemlost => 1 });
545
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall that is marked lost" );
546
    $item1->update({ itemlost => 0, withdrawn => 1 });
547
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall that is withdrawn" );
548
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall item if not checked out" );
549
550
    $item1->update({ withdrawn => 0 });
551
    C4::Circulation::AddIssue( $patron2->unblessed, $item1->barcode );
552
553
    Koha::CirculationRules->set_rules({
554
        branchcode => $branchcode,
555
        categorycode => $patron1->categorycode,
556
        itemtype => $item1->effective_itemtype,
557
        rules => {
558
            recalls_allowed => 0,
559
            recalls_per_record => 1,
560
            on_shelf_recalls => 'all',
561
        },
562
    });
563
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if recalls_allowed = 0" );
564
565
    Koha::CirculationRules->set_rules({
566
        branchcode => $branchcode,
567
        categorycode => $patron1->categorycode,
568
        itemtype => $item1->effective_itemtype,
569
        rules => {
570
            recalls_allowed => 1,
571
            recalls_per_record => 1,
572
            on_shelf_recalls => 'all',
573
        },
574
    });
575
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has more existing recall(s) than recalls_allowed" );
576
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has more existing recall(s) than recalls_per_record" );
577
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has already recalled this item" );
578
579
    my $reserve_id = C4::Reserves::AddReserve({ branchcode => $branchcode, borrowernumber => $patron1->borrowernumber, biblionumber => $item1->biblionumber, itemnumber => $item1->itemnumber });
580
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall item if patron has already reserved it" );
581
    C4::Reserves::ModReserve({ rank => 'del', reserve_id => $reserve_id, branchcode => $branchcode, itemnumber => $item1->itemnumber, borrowernumber => $patron1->borrowernumber, biblionumber => $item1->biblionumber });
582
583
    $recall1->set_cancelled;
584
    is( $item1->can_be_recalled({ patron => $patron2 }), 0, "Can't recall if patron has already checked out an item attached to this biblio" );
585
586
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if on_shelf_recalls = all and items are still available" );
587
588
    Koha::CirculationRules->set_rules({
589
        branchcode => $branchcode,
590
        categorycode => $patron1->categorycode,
591
        itemtype => $item1->effective_itemtype,
592
        rules => {
593
            recalls_allowed => 1,
594
            recalls_per_record => 1,
595
            on_shelf_recalls => 'any',
596
        },
597
    });
598
    C4::Circulation::AddReturn( $item1->barcode, $branchcode );
599
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if no items are checked out" );
600
601
    C4::Circulation::AddIssue( $patron2->unblessed, $item1->barcode );
602
    is( $item1->can_be_recalled({ patron => $patron1 }), 1, "Can recall item" );
603
604
    $recall1 = Koha::Recall->new({
605
        borrowernumber => $patron1->borrowernumber,
606
        recalldate => Koha::DateUtils::dt_from_string,
607
        biblionumber => $biblio->biblionumber,
608
        branchcode => $branchcode,
609
        status => 'R',
610
        itemnumber => undef,
611
        expirationdate => undef,
612
        item_level_recall => 0
613
    })->store;
614
615
    # Patron2 has Item1 checked out. Patron1 has placed a biblio-level recall on Biblio1, so check if Item1 can fulfill Patron1's recall.
616
617
    Koha::CirculationRules->set_rules({
618
        branchcode => undef,
619
        categorycode => undef,
620
        itemtype => $item1->effective_itemtype,
621
        rules => {
622
            recalls_allowed => 0,
623
            recalls_per_record => 1,
624
            on_shelf_recalls => 'any',
625
        },
626
    });
627
    is( $item1->can_be_waiting_recall, 0, "Recalls not allowed for this itemtype" );
628
629
    Koha::CirculationRules->set_rules({
630
        branchcode => undef,
631
        categorycode => undef,
632
        itemtype => $item1->effective_itemtype,
633
        rules => {
634
            recalls_allowed => 1,
635
            recalls_per_record => 1,
636
            on_shelf_recalls => 'any',
637
        },
638
    });
639
    is( $item1->can_be_waiting_recall, 1, "Recalls are allowed for this itemtype" );
640
};

Return to bug 19532