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

(-)a/C4/Circulation.pm (-2 / +101 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 and C4::Context->preference('UseRecalls') ) {
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_waiting_recall ) {
1053
        my @recalls = $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} }) if C4::Context->preference('UseRecalls');
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 and
2229
         $transfer_recall->branchcode eq $branch and
2230
         C4::Context->preference('UseRecalls') ) {
2231
        $messages->{TransferredRecall} = $transfer_recall;
2232
    }
2233
2146
    # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
2234
    # 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'}){
2235
    if (!$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} ){
2148
        my $BranchTransferLimitsType = C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ? 'effective_itemtype' : 'ccode';
2236
        my $BranchTransferLimitsType = C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ? 'effective_itemtype' : 'ccode';
2149
        if  (C4::Context->preference("AutomaticItemReturn"    ) or
2237
        if  (C4::Context->preference("AutomaticItemReturn"    ) or
2150
            (C4::Context->preference("UseBranchTransferLimits") and
2238
            (C4::Context->preference("UseBranchTransferLimits") and
Lines 2835-2840 sub CanBookBeRenewed { Link Here
2835
        }
2923
        }
2836
    }
2924
    }
2837
2925
2926
    my $recall = $item->check_recalls if $item->can_be_waiting_recall;
2927
    if ( defined $recall ) {
2928
        if ( $recall->item_level_recall ) {
2929
            # item-level recall. check if this item is the recalled item, otherwise renewal will be allowed
2930
            return ( 0, 'recalled' ) if ( $recall->itemnumber == $item->itemnumber );
2931
        } else {
2932
            # biblio-level recall, so only disallow renewal if the biblio-level recall has been fulfilled by a different item
2933
            return ( 0, 'recalled' ) unless ( $recall->waiting );
2934
        }
2935
    }
2936
2838
    my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2937
    my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2839
2938
2840
    # This item can fill one or more unfilled reserve, can those unfilled reserves
2939
    # 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 / +7 lines)
Lines 321-327 sub buildKohaItemsNamespace { Link Here
321
        my $status;
321
        my $status;
322
        my $substatus = '';
322
        my $substatus = '';
323
323
324
        if ($item->has_pending_hold) {
324
        my $recalls = Koha::Recalls->search({ itemnumber => $item->itemnumber, status => 'W' });
325
326
        if ( $recalls->count ) {
327
            # recalls take priority over holds
328
            $status = 'Waiting';
329
        }
330
        elsif ( $item->has_pending_hold ) {
325
            $status = 'Pending hold';
331
            $status = 'Pending hold';
326
        }
332
        }
327
        elsif ( $item->holds->waiting->count ) {
333
        elsif ( $item->holds->waiting->count ) {
(-)a/Koha/Biblio.pm (+102 lines)
Lines 816-821 sub to_api_mapping { Link Here
816
    };
816
    };
817
}
817
}
818
818
819
=head3 recalls
820
821
    my @recalls = $biblio->recalls;
822
823
Return all active recalls attached to this biblio, sorted by oldest first
824
825
=cut
826
827
sub recalls {
828
    my ( $self ) = @_;
829
    my @recalls_rs = Koha::Recalls->search({ biblionumber => $self->biblionumber, old => undef }, { order_by => { -asc => 'recalldate' } });
830
    return @recalls_rs;
831
}
832
833
=head3 can_be_recalled
834
835
    my @items_for_recall = $biblio->can_be_recalled({ patron => $patron_object });
836
837
Does biblio-level checks and returns the items attached to this biblio that are available for recall
838
839
=cut
840
841
sub can_be_recalled {
842
    my ( $self, $params ) = @_;
843
844
    return 0 if !( C4::Context->preference('UseRecalls') );
845
846
    my $patron = $params->{patron};
847
848
    my $branchcode = C4::Context->userenv->{'branch'};
849
    if ( C4::Context->preference('CircControl') eq 'PatronLibrary' and $patron ) {
850
        $branchcode = $patron->branchcode;
851
    }
852
853
    # items are unavailable for recall if they are lost, withdrawn or notforloan
854
    my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 });
855
856
    # if there are no available items at all, no recall can be placed
857
    return 0 if ( scalar @items == 0 );
858
859
    my @itemtypes;
860
    my @itemnumbers;
861
    foreach my $item ( @items ) {
862
        push( @itemtypes, $item->effective_itemtype );
863
        push( @itemnumbers, $item->itemnumber );
864
    }
865
866
    # Check the circulation rule for each relevant itemtype for this biblio
867
    my ( @recalls_allowed, @recalls_per_record, @on_shelf_recalls );
868
    foreach my $itemtype ( @itemtypes ) {
869
        my $rule = Koha::CirculationRules->get_effective_rules({
870
            branchcode => $branchcode,
871
            categorycode => $patron ? $patron->categorycode : undef,
872
            itemtype => $itemtype,
873
            rules => [
874
                'recalls_allowed',
875
                'recalls_per_record',
876
                'on_shelf_recalls',
877
            ],
878
        });
879
        push( @recalls_allowed, $rule->{recalls_allowed} ) if $rule;
880
        push( @recalls_per_record, $rule->{recalls_per_record} ) if $rule;
881
        push( @on_shelf_recalls, $rule->{on_shelf_recalls} ) if $rule;
882
    }
883
    my $recalls_allowed = (sort {$b <=> $a} @recalls_allowed)[0]; # take highest
884
    my $recalls_per_record = (sort {$b <=> $a} @recalls_per_record)[0]; # take highest
885
    my %on_shelf_recalls_count = ();
886
    foreach my $count ( @on_shelf_recalls ) {
887
        $on_shelf_recalls_count{$count}++;
888
    }
889
    my $on_shelf_recalls = (sort {$on_shelf_recalls_count{$b} <=> $on_shelf_recalls_count{$a}} @on_shelf_recalls)[0]; # take most common
890
891
    # check recalls allowed has been set and is not zero
892
    return 0 if ( !defined($recalls_allowed) || $recalls_allowed == 0 );
893
894
    if ( $patron ) {
895
        # check borrower has not reached open recalls allowed limit
896
        return 0 if ( $patron->recalls->count >= $recalls_allowed );
897
898
        # check borrower has not reached open recalls allowed per record limit
899
        return 0 if ( $patron->recalls({ biblionumber => $self->biblionumber })->count >= $recalls_per_record );
900
901
        # check if any of the items under this biblio are already checked out by this borrower
902
        return 0 if ( Koha::Checkouts->search({ itemnumber => [ @itemnumbers ], borrowernumber => $patron->borrowernumber })->count > 0 );
903
    }
904
905
    # check item availability
906
    my $checked_out_count = 0;
907
    foreach (@items) {
908
        if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
909
    }
910
911
    # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
912
    return 0 if ( $on_shelf_recalls eq 'all' && $checked_out_count < scalar @items );
913
914
    # can't recall if no items have been checked out
915
    return 0 if ( $checked_out_count == 0 );
916
917
    # can recall
918
    return @items;
919
}
920
819
=head2 Internal methods
921
=head2 Internal methods
820
922
821
=head3 type
923
=head3 type
(-)a/Koha/Item.pm (+181 lines)
Lines 852-857 sub _after_item_action_hooks { Link Here
852
    }
852
    }
853
}
853
}
854
854
855
=head3 recall
856
857
    my $recall = $item->recall;
858
859
Return the relevant recall for this item
860
861
=cut
862
863
sub recall {
864
    my ( $self ) = @_;
865
    my @recalls = Koha::Recalls->search({ biblionumber => $self->biblionumber, old => undef }, { order_by => { -asc => 'recalldate' } });
866
    foreach my $recall (@recalls) {
867
        if ( $recall->item_level_recall and $recall->itemnumber == $self->itemnumber ){
868
            return $recall;
869
        }
870
    }
871
    # no item-level recall to return, so return earliest biblio-level
872
    # FIXME: eventually this will be based on priority
873
    return $recalls[0];
874
}
875
876
=head3 can_be_recalled
877
878
    if ( $item->can_be_recalled({ patron => $patron_object }) ) # do recall
879
880
Does item-level checks and returns if items can be recalled by this borrower
881
882
=cut
883
884
sub can_be_recalled {
885
    my ( $self, $params ) = @_;
886
887
    return 0 if !( C4::Context->preference('UseRecalls') );
888
889
    # check if this item is not for loan, withdrawn or lost
890
    return 0 if ( $self->notforloan != 0 );
891
    return 0 if ( $self->itemlost != 0 );
892
    return 0 if ( $self->withdrawn != 0 );
893
894
    # check if this item is not checked out - if not checked out, can't be recalled
895
    return 0 if ( !defined( $self->checkout ) );
896
897
    my $patron = $params->{patron};
898
899
    my $branchcode = C4::Context->userenv->{'branch'};
900
    if ( $patron ) {
901
        $branchcode = C4::Circulation::_GetCircControlBranch( $self->unblessed, $patron->unblessed );
902
    }
903
904
    # Check the circulation rule for each relevant itemtype for this item
905
    my $rule = Koha::CirculationRules->get_effective_rules({
906
        branchcode => $branchcode,
907
        categorycode => $patron ? $patron->categorycode : undef,
908
        itemtype => $self->effective_itemtype,
909
        rules => [
910
            'recalls_allowed',
911
            'recalls_per_record',
912
            'on_shelf_recalls',
913
        ],
914
    });
915
916
    # check recalls allowed has been set and is not zero
917
    return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
918
919
    if ( $patron ) {
920
        # check borrower has not reached open recalls allowed limit
921
        return 0 if ( $patron->recalls->count >= $rule->{recalls_allowed} );
922
923
        # check borrower has not reach open recalls allowed per record limit
924
        return 0 if ( $patron->recalls({ biblionumber => $self->biblionumber })->count >= $rule->{recalls_per_record} );
925
926
        # check if this patron has already recalled this item
927
        return 0 if ( Koha::Recalls->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber, old => undef })->count > 0 );
928
929
        # check if this patron has already checked out this item
930
        return 0 if ( Koha::Checkouts->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
931
932
        # check if this patron has already reserved this item
933
        return 0 if ( Koha::Holds->search({ itemnumber => $self->itemnumber, borrowernumber => $patron->borrowernumber })->count > 0 );
934
    }
935
936
    # check item availability
937
    # items are unavailable for recall if they are lost, withdrawn or notforloan
938
    my @items = Koha::Items->search({ biblionumber => $self->biblionumber, itemlost => 0, withdrawn => 0, notforloan => 0 });
939
940
    # if there are no available items at all, no recall can be placed
941
    return 0 if ( scalar @items == 0 );
942
943
    my $checked_out_count = 0;
944
    foreach (@items) {
945
        if ( Koha::Checkouts->search({ itemnumber => $_->itemnumber })->count > 0 ){ $checked_out_count++; }
946
    }
947
948
    # can't recall if on shelf recalls only allowed when all unavailable, but items are still available for checkout
949
    return 0 if ( $rule->{on_shelf_recalls} eq 'all' && $checked_out_count < scalar @items );
950
951
    # can't recall if no items have been checked out
952
    return 0 if ( $checked_out_count == 0 );
953
954
    # can recall
955
    return 1;
956
}
957
958
=head3 can_be_waiting_recall
959
960
    if ( $item->can_be_waiting_recall ) { # allocate item as waiting for recall
961
962
Checks item type and branch of circ rules to return whether this item can be used to fill a recall.
963
At this point the item has already been recalled. We are now at the checkin and set waiting stage.
964
965
=cut
966
967
sub can_be_waiting_recall {
968
    my ( $self ) = @_;
969
970
    return 0 if !( C4::Context->preference('UseRecalls') );
971
972
    # check if this item is not for loan, withdrawn or lost
973
    return 0 if ( $self->notforloan != 0 );
974
    return 0 if ( $self->itemlost != 0 );
975
    return 0 if ( $self->withdrawn != 0 );
976
977
    my $branchcode = $self->holdingbranch;
978
    if ( C4::Context->preference('CircControl') eq 'PickupLibrary' and C4::Context->userenv and C4::Context->userenv->{'branch'} ) {
979
        $branchcode = C4::Context->userenv->{'branch'};
980
    } else {
981
        $branchcode = ( C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ) ? $self->homebranch : $self->holdingbranch;
982
    }
983
984
    # Check the circulation rule for each relevant itemtype for this item
985
    my $rule = Koha::CirculationRules->get_effective_rules({
986
        branchcode => $branchcode,
987
        categorycode => undef,
988
        itemtype => $self->effective_itemtype,
989
        rules => [
990
            'recalls_allowed',
991
        ],
992
    });
993
994
    # check recalls allowed has been set and is not zero
995
    return 0 if ( !defined($rule->{recalls_allowed}) || $rule->{recalls_allowed} == 0 );
996
997
    # can recall
998
    return 1;
999
}
1000
1001
=head3 check_recalls
1002
1003
    my $recall = $item->check_recalls;
1004
1005
Get the most relevant recall for this item.
1006
1007
=cut
1008
1009
sub check_recalls {
1010
    my ( $self ) = @_;
1011
1012
    my @recalls = Koha::Recalls->search({ itemnumber => $self->itemnumber, status => [ 'R','O','W' ] }, { order_by => { -asc => 'recalldate' } });
1013
    if ( scalar @recalls == 0 ) {
1014
        # no item-level recalls or recalls waiting for this specific item
1015
        # look for biblio-level recalls
1016
        @recalls = Koha::Recalls->search({ biblionumber => $self->biblionumber, itemnumber => [ $self->itemnumber, undef ], status => [ 'R','O','W' ] }, { order_by => { -asc => 'recalldate' } });
1017
    }
1018
1019
    my $recall;
1020
    # iterate through relevant recalls to find the best one.
1021
    # if we come across a waiting recall, use this one.
1022
    # 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.
1023
    foreach my $r ( @recalls ) {
1024
        if ( $r->waiting ) {
1025
            $recall = $r;
1026
            last;
1027
        }
1028
    }
1029
    unless ( defined $recall ) {
1030
        $recall = $recalls[0];
1031
    }
1032
1033
    return $recall;
1034
}
1035
855
=head3 _type
1036
=head3 _type
856
1037
857
=cut
1038
=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/Circulation.t (-2 / +243 lines)
Lines 18-24 Link Here
18
use Modern::Perl;
18
use Modern::Perl;
19
use utf8;
19
use utf8;
20
20
21
use Test::More tests => 48;
21
use Test::More tests => 51;
22
use Test::MockModule;
22
use Test::MockModule;
23
use Test::Deep qw( cmp_deeply );
23
use Test::Deep qw( cmp_deeply );
24
24
Lines 273-279 Koha::CirculationRules->set_rules( Link Here
273
273
274
my ( $reused_itemnumber_1, $reused_itemnumber_2 );
274
my ( $reused_itemnumber_1, $reused_itemnumber_2 );
275
subtest "CanBookBeRenewed tests" => sub {
275
subtest "CanBookBeRenewed tests" => sub {
276
    plan tests => 83;
276
    plan tests => 87;
277
277
278
    C4::Context->set_preference('ItemsDeniedRenewal','');
278
    C4::Context->set_preference('ItemsDeniedRenewal','');
279
    # Generate test biblio
279
    # Generate test biblio
Lines 1256-1261 subtest "CanBookBeRenewed tests" => sub { Link Here
1256
            $item_3->itemcallnumber || '' ),
1256
            $item_3->itemcallnumber || '' ),
1257
        "Account line description must not contain 'Lost Items ', but be title, barcode, itemcallnumber"
1257
        "Account line description must not contain 'Lost Items ', but be title, barcode, itemcallnumber"
1258
    );
1258
    );
1259
1260
    # Recalls
1261
    t::lib::Mocks::mock_preference('UseRecalls', 1);
1262
    Koha::CirculationRules->set_rules({
1263
        categorycode => undef,
1264
        branchcode => undef,
1265
        itemtype => undef,
1266
        rules => {
1267
            recalls_allowed => 10,
1268
            renewalsallowed => 5,
1269
        },
1270
    });
1271
    my $recall_borrower = $builder->build_object({ class => 'Koha::Patrons' });
1272
    my $recall_biblio = $builder->build_object({ class => 'Koha::Biblios' });
1273
    my $recall_item1 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $recall_biblio->biblionumber } });
1274
    my $recall_item2 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $recall_biblio->biblionumber } });
1275
1276
    AddIssue( $renewing_borrower, $recall_item1->barcode );
1277
1278
    # item-level and this item: renewal not allowed
1279
    my $recall = Koha::Recall->new({
1280
        biblionumber => $recall_item1->biblionumber,
1281
        itemnumber => $recall_item1->itemnumber,
1282
        borrowernumber => $recall_borrower->borrowernumber,
1283
        branchcode => $recall_borrower->branchcode,
1284
        item_level_recall => 1,
1285
        status => 'R',
1286
    })->store;
1287
    ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $recall_item1->itemnumber );
1288
    is( $error, 'recalled', 'Cannot renew item that has been recalled' );
1289
    $recall->set_cancelled;
1290
1291
    # biblio-level requested recall: renewal not allowed
1292
    $recall = Koha::Recall->new({
1293
        biblionumber => $recall_item1->biblionumber,
1294
        itemnumber => undef,
1295
        borrowernumber => $recall_borrower->borrowernumber,
1296
        branchcode => $recall_borrower->branchcode,
1297
        item_level_recall => 0,
1298
        status => 'R',
1299
    })->store;
1300
    ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $recall_item1->itemnumber );
1301
    is( $error, 'recalled', 'Cannot renew item if biblio is recalled and has no item allocated' );
1302
    $recall->set_cancelled;
1303
1304
    # item-level and not this item: renewal allowed
1305
    $recall = Koha::Recall->new({
1306
        biblionumber => $recall_item2->biblionumber,
1307
        itemnumber => $recall_item2->itemnumber,
1308
        borrowernumber => $recall_borrower->borrowernumber,
1309
        branchcode => $recall_borrower->branchcode,
1310
        item_level_recall => 1,
1311
        status => 'R',
1312
    })->store;
1313
    ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $recall_item1->itemnumber );
1314
    is( $renewokay, 1, 'Can renew item if item-level recall on biblio is not on this item' );
1315
    $recall->set_cancelled;
1316
1317
    # biblio-level waiting recall: renewal allowed
1318
    $recall = Koha::Recall->new({
1319
        biblionumber => $recall_item1->biblionumber,
1320
        itemnumber => undef,
1321
        borrowernumber => $recall_borrower->borrowernumber,
1322
        branchcode => $recall_borrower->branchcode,
1323
        item_level_recall => 0,
1324
        status => 'R',
1325
    })->store;
1326
    $recall->set_waiting({ item => $recall_item1 });
1327
    ( $renewokay, $error ) = CanBookBeRenewed( $renewing_borrowernumber, $recall_item1->itemnumber );
1328
    is( $renewokay, 1, 'Can renew item if biblio-level recall has already been allocated an item' );
1329
    $recall->set_cancelled;
1259
};
1330
};
1260
1331
1261
subtest "GetUpcomingDueIssues" => sub {
1332
subtest "GetUpcomingDueIssues" => sub {
Lines 1736-1741 subtest 'AddIssue & AllowReturnToBranch' => sub { Link Here
1736
    # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
1807
    # TODO t::lib::Mocks::mock_preference('AllowReturnToBranch', 'homeorholdingbranch');
1737
};
1808
};
1738
1809
1810
subtest 'AddIssue | recalls' => sub {
1811
    plan tests => 3;
1812
1813
    t::lib::Mocks::mock_preference("UseRecalls", 1);
1814
    t::lib::Mocks::mock_preference("item-level_itypes", 1);
1815
    my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
1816
    my $patron2 = $builder->build_object({ class => 'Koha::Patrons' });
1817
    my $item = $builder->build_sample_item;
1818
    Koha::CirculationRules->set_rules({
1819
        branchcode => undef,
1820
        itemtype => undef,
1821
        categorycode => undef,
1822
        rules => {
1823
            recalls_allowed => 10,
1824
        },
1825
    });
1826
1827
    # checking out item that they have recalled
1828
    my $recall1 = Koha::Recall->new({
1829
        borrowernumber => $patron1->borrowernumber,
1830
        biblionumber => $item->biblionumber,
1831
        itemnumber => $item->itemnumber,
1832
        item_level_recall => 1,
1833
        branchcode => $patron1->branchcode,
1834
        status => 'R',
1835
    })->store;
1836
    AddIssue( $patron1->unblessed, $item->barcode, undef, undef, undef, undef, { recall_id => $recall1->recall_id } );
1837
    $recall1 = Koha::Recalls->find( $recall1->recall_id );
1838
    is( $recall1->finished, 1, 'Recall was fulfilled when patron checked out item' );
1839
    AddReturn( $item->barcode, $item->homebranch );
1840
1841
    # this item is has a recall request. cancel recall
1842
    my $recall2 = Koha::Recall->new({
1843
        borrowernumber => $patron2->borrowernumber,
1844
        biblionumber => $item->biblionumber,
1845
        itemnumber => $item->itemnumber,
1846
        item_level_recall => 1,
1847
        branchcode => $patron2->branchcode,
1848
        status => 'R',
1849
    })->store;
1850
    AddIssue( $patron1->unblessed, $item->barcode, undef, undef, undef, undef, { recall_id => $recall2->recall_id, cancel_recall => 'cancel' } );
1851
    $recall2 = Koha::Recalls->find( $recall2->recall_id );
1852
    is( $recall2->cancelled, 1, 'Recall was cancelled when patron checked out item' );
1853
    AddReturn( $item->barcode, $item->homebranch );
1854
1855
    # this item is waiting to fulfill a recall. revert recall
1856
    my $recall3 = Koha::Recall->new({
1857
        borrowernumber => $patron2->borrowernumber,
1858
        biblionumber => $item->biblionumber,
1859
        itemnumber => $item->itemnumber,
1860
        item_level_recall => 1,
1861
        branchcode => $patron2->branchcode,
1862
        status => 'R',
1863
    })->store;
1864
    $recall3->set_waiting;
1865
    AddIssue( $patron1->unblessed, $item->barcode, undef, undef, undef, undef, { recall_id => $recall3->recall_id, cancel_recall => 'revert' } );
1866
    $recall3 = Koha::Recalls->find( $recall3->recall_id );
1867
    is( $recall3->requested, 1, 'Recall was reverted from waiting when patron checked out item' );
1868
    AddReturn( $item->barcode, $item->homebranch );
1869
};
1870
1871
1739
subtest 'CanBookBeIssued + Koha::Patron->is_debarred|has_overdues' => sub {
1872
subtest 'CanBookBeIssued + Koha::Patron->is_debarred|has_overdues' => sub {
1740
    plan tests => 8;
1873
    plan tests => 8;
1741
1874
Lines 3396-3401 subtest 'CanBookBeIssued | notforloan' => sub { Link Here
3396
    # TODO test with AllowNotForLoanOverride = 1
3529
    # TODO test with AllowNotForLoanOverride = 1
3397
};
3530
};
3398
3531
3532
subtest 'CanBookBeIssued | recalls' => sub {
3533
    plan tests => 2;
3534
3535
    t::lib::Mocks::mock_preference("UseRecalls", 1);
3536
    t::lib::Mocks::mock_preference("item-level_itypes", 1);
3537
    my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
3538
    my $patron2 = $builder->build_object({ class => 'Koha::Patrons' });
3539
    my $item = $builder->build_sample_item;
3540
    Koha::CirculationRules->set_rules({
3541
        branchcode => undef,
3542
        itemtype => undef,
3543
        categorycode => undef,
3544
        rules => {
3545
            recalls_allowed => 10,
3546
        },
3547
    });
3548
3549
    # item-level recall
3550
    my $recall = Koha::Recall->new({
3551
        borrowernumber => $patron1->borrowernumber,
3552
        biblionumber => $item->biblionumber,
3553
        itemnumber => $item->itemnumber,
3554
        item_level_recall => 1,
3555
        branchcode => $patron1->branchcode,
3556
        status => 'R',
3557
    })->store;
3558
3559
    my ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron2, $item->barcode, undef, undef, undef, undef );
3560
    is( $needsconfirmation->{RECALLED}->recall_id, $recall->recall_id, "Another patron has placed an item-level recall on this item" );
3561
3562
    $recall->set_cancelled;
3563
3564
    # biblio-level recall
3565
    $recall = Koha::Recall->new({
3566
        borrowernumber => $patron1->borrowernumber,
3567
        biblionumber => $item->biblionumber,
3568
        itemnumber => undef,
3569
        item_level_recall => 0,
3570
        branchcode => $patron1->branchcode,
3571
        status => 'R',
3572
    })->store;
3573
3574
    ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $patron2, $item->barcode, undef, undef, undef, undef );
3575
    is( $needsconfirmation->{RECALLED}->recall_id, $recall->recall_id, "Another patron has placed a biblio-level recall and this item is eligible to fill it" );
3576
3577
    $recall->set_cancelled;
3578
};
3579
3399
subtest 'AddReturn should clear items.onloan for unissued items' => sub {
3580
subtest 'AddReturn should clear items.onloan for unissued items' => sub {
3400
    plan tests => 1;
3581
    plan tests => 1;
3401
3582
Lines 3411-3416 subtest 'AddReturn should clear items.onloan for unissued items' => sub { Link Here
3411
    is( $item->onloan, undef, 'AddReturn did clear items.onloan' );
3592
    is( $item->onloan, undef, 'AddReturn did clear items.onloan' );
3412
};
3593
};
3413
3594
3595
subtest 'AddReturn | recalls' => sub {
3596
    plan tests => 3;
3597
3598
    t::lib::Mocks::mock_preference("UseRecalls", 1);
3599
    t::lib::Mocks::mock_preference("item-level_itypes", 1);
3600
    my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
3601
    my $patron2 = $builder->build_object({ class => 'Koha::Patrons' });
3602
    my $item1 = $builder->build_sample_item;
3603
    Koha::CirculationRules->set_rules({
3604
        branchcode => undef,
3605
        itemtype => undef,
3606
        categorycode => undef,
3607
        rules => {
3608
            recalls_allowed => 10,
3609
        },
3610
    });
3611
3612
    # this item can fill a recall with pickup at this branch
3613
    AddIssue( $patron1->unblessed, $item1->barcode );
3614
    my $recall1 = Koha::Recall->new({
3615
        borrowernumber => $patron2->borrowernumber,
3616
        biblionumber => $item1->biblionumber,
3617
        itemnumber => $item1->itemnumber,
3618
        item_level_recall => 1,
3619
        branchcode => $item1->homebranch,
3620
        status => 'R',
3621
    })->store;
3622
    my ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $item1->barcode, $item1->homebranch );
3623
    is( $messages->{RecallFound}->recall_id, $recall1->recall_id, "Recall found" );
3624
    $recall1->set_cancelled;
3625
3626
    # this item can fill a recall but needs transfer
3627
    AddIssue( $patron1->unblessed, $item1->barcode );
3628
    $recall1 = Koha::Recall->new({
3629
        borrowernumber => $patron2->borrowernumber,
3630
        biblionumber => $item1->biblionumber,
3631
        itemnumber => $item1->itemnumber,
3632
        item_level_recall => 1,
3633
        branchcode => $patron2->branchcode,
3634
        status => 'R',
3635
    })->store;
3636
    ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $item1->barcode, $item1->homebranch );
3637
    is( $messages->{RecallNeedsTransfer}, $item1->homebranch, "Recall requiring transfer found" );
3638
    $recall1->set_cancelled;
3639
3640
    # this item is already in transit, do not ask to transfer
3641
    AddIssue( $patron1->unblessed, $item1->barcode );
3642
    $recall1 = Koha::Recall->new({
3643
        borrowernumber => $patron2->borrowernumber,
3644
        biblionumber => $item1->biblionumber,
3645
        itemnumber => $item1->itemnumber,
3646
        item_level_recall => 1,
3647
        branchcode => $patron2->branchcode,
3648
        status => 'R',
3649
    })->store;
3650
    $recall1->start_transfer;
3651
    ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $item1->barcode, $patron2->branchcode );
3652
    is( $messages->{TransferredRecall}->recall_id, $recall1->recall_id, "In transit recall found" );
3653
    $recall1->set_cancelled;
3654
};
3414
3655
3415
subtest 'AddRenewal and AddIssuingCharge tests' => sub {
3656
subtest 'AddRenewal and AddIssuingCharge tests' => sub {
3416
3657
(-)a/t/db_dependent/Circulation/transferbook.t (-1 / +28 lines)
Lines 90-96 subtest 'field population tests' => sub { Link Here
90
#FIXME:'UseBranchTransferLimits tests missing
90
#FIXME:'UseBranchTransferLimits tests missing
91
91
92
subtest 'transfer already at destination' => sub {
92
subtest 'transfer already at destination' => sub {
93
    plan tests => 5;
93
    plan tests => 9;
94
94
95
    my $library = $builder->build_object( { class => 'Koha::Libraries' } )->store;
95
    my $library = $builder->build_object( { class => 'Koha::Libraries' } )->store;
96
    t::lib::Mocks::mock_userenv( { branchcode => $library->branchcode } );
96
    t::lib::Mocks::mock_userenv( { branchcode => $library->branchcode } );
Lines 130-135 subtest 'transfer already at destination' => sub { Link Here
130
    is( $dotransfer, 1, 'Transfer of reserved item succeeded without ignore reserves' );
130
    is( $dotransfer, 1, 'Transfer of reserved item succeeded without ignore reserves' );
131
    is( $messages->{ResFound}->{ResFound}, 'Reserved', "We found the reserve");
131
    is( $messages->{ResFound}->{ResFound}, 'Reserved', "We found the reserve");
132
    is( $messages->{ResFound}->{itemnumber}, $item->itemnumber, "We got the reserve info");
132
    is( $messages->{ResFound}->{itemnumber}, $item->itemnumber, "We got the reserve info");
133
134
    # recalls
135
    t::lib::Mocks::mock_preference('UseRecalls', 1);
136
    my $recall = Koha::Recall->new({
137
        biblionumber => $item->biblionumber,
138
        itemnumber => $item->itemnumber,
139
        item_level_recall => 1,
140
        borrowernumber => $patron->borrowernumber,
141
        branchcode => $library->branchcode,
142
        status => 'R',
143
    })->store;
144
    ( $recall, $dotransfer, $messages ) = $recall->start_transfer;
145
    is( $dotransfer, 0, 'Do not transfer recalled item, it has already arrived' );
146
    is( $messages->{RecallPlacedAtHoldingBranch}, 1, "We found the recall");
147
148
    my $item2 = $builder->build_object({ class => 'Koha::Items' }); # this item will have a different holding branch to the pickup branch
149
    $recall = Koha::Recall->new({
150
        biblionumber => $item2->biblionumber,
151
        itemnumber => $item2->itemnumber,
152
        item_level_recall => 1,
153
        borrowernumber => $patron->borrowernumber,
154
        branchcode => $library->branchcode,
155
        status => 'R',
156
    })->store;
157
    ( $recall, $dotransfer, $messages ) = $recall->start_transfer;
158
    is( $dotransfer, 1, 'Transfer of recalled item succeeded' );
159
    is( $messages->{RecallFound}->recall_id, $recall->recall_id, "We found the recall");
133
};
160
};
134
161
135
subtest 'transfer an issued item' => sub {
162
subtest 'transfer an issued item' => sub {
(-)a/t/db_dependent/Koha/Biblio.t (-2 / +118 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 184-194 subtest 'pickup_locations' => sub { Link Here
184
184
185
    # Cleanup database
185
    # Cleanup database
186
    Koha::Holds->search->delete;
186
    Koha::Holds->search->delete;
187
    $dbh->do('DELETE FROM issues');
187
    Koha::Patrons->search->delete;
188
    Koha::Patrons->search->delete;
188
    Koha::Items->search->delete;
189
    Koha::Items->search->delete;
189
    Koha::Libraries->search->delete;
190
    Koha::Libraries->search->delete;
190
    Koha::CirculationRules->search->delete;
191
    Koha::CirculationRules->search->delete;
191
    $dbh->do('DELETE FROM issues');
192
    Koha::CirculationRules->set_rules(
192
    Koha::CirculationRules->set_rules(
193
        {
193
        {
194
            categorycode => undef,
194
            categorycode => undef,
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 = scalar $biblio->recalls;
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 = scalar $biblio->recalls;
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 (-1 / +188 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 501-503 subtest 'renewal_branchcode' => sub { Link Here
501
    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");
501
    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");
502
502
503
};
503
};
504
505
subtest 'Recalls tests' => sub {
506
507
    plan tests => 20;
508
509
    $schema->storage->txn_begin;
510
511
    my $item1 = $builder->build_sample_item;
512
    my $biblio = $item1->biblio;
513
    my $branchcode = $item1->holdingbranch;
514
    my $patron1 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } });
515
    my $patron2 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } });
516
    my $patron3 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } });
517
    my $item2 = $builder->build_object({ class => 'Koha::Items', value => { holdingbranch => $branchcode, homebranch => $branchcode, biblionumber => $biblio->biblionumber, itype => $item1->effective_itemtype } });
518
    t::lib::Mocks::mock_userenv({ patron => $patron1 });
519
520
    my $recall1 = Koha::Recall->new({
521
        borrowernumber => $patron1->borrowernumber,
522
        recalldate => Koha::DateUtils::dt_from_string,
523
        biblionumber => $biblio->biblionumber,
524
        branchcode => $branchcode,
525
        status => 'R',
526
        itemnumber => $item1->itemnumber,
527
        expirationdate => undef,
528
        item_level_recall => 1
529
    })->store;
530
    my $recall2 = Koha::Recall->new({
531
        borrowernumber => $patron2->borrowernumber,
532
        recalldate => Koha::DateUtils::dt_from_string,
533
        biblionumber => $biblio->biblionumber,
534
        branchcode => $branchcode,
535
        status => 'R',
536
        itemnumber => $item1->itemnumber,
537
        expirationdate => undef,
538
        item_level_recall =>1
539
    })->store;
540
541
    is( $item1->recall->borrowernumber, $patron1->borrowernumber, 'Correctly returns most relevant recall' );
542
543
    $recall2->set_cancelled;
544
545
    t::lib::Mocks::mock_preference('UseRecalls', 0);
546
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall with UseRecalls disabled" );
547
548
    t::lib::Mocks::mock_preference("UseRecalls", 1);
549
550
    $item1->update({ notforloan => 1 });
551
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall that is not for loan" );
552
    $item1->update({ notforloan => 0, itemlost => 1 });
553
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall that is marked lost" );
554
    $item1->update({ itemlost => 0, withdrawn => 1 });
555
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall that is withdrawn" );
556
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall item if not checked out" );
557
558
    $item1->update({ withdrawn => 0 });
559
    C4::Circulation::AddIssue( $patron2->unblessed, $item1->barcode );
560
561
    Koha::CirculationRules->set_rules({
562
        branchcode => $branchcode,
563
        categorycode => $patron1->categorycode,
564
        itemtype => $item1->effective_itemtype,
565
        rules => {
566
            recalls_allowed => 0,
567
            recalls_per_record => 1,
568
            on_shelf_recalls => 'all',
569
        },
570
    });
571
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if recalls_allowed = 0" );
572
573
    Koha::CirculationRules->set_rules({
574
        branchcode => $branchcode,
575
        categorycode => $patron1->categorycode,
576
        itemtype => $item1->effective_itemtype,
577
        rules => {
578
            recalls_allowed => 1,
579
            recalls_per_record => 1,
580
            on_shelf_recalls => 'all',
581
        },
582
    });
583
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has more existing recall(s) than recalls_allowed" );
584
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has more existing recall(s) than recalls_per_record" );
585
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has already recalled this item" );
586
587
    my $reserve_id = C4::Reserves::AddReserve({ branchcode => $branchcode, borrowernumber => $patron1->borrowernumber, biblionumber => $item1->biblionumber, itemnumber => $item1->itemnumber });
588
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall item if patron has already reserved it" );
589
    C4::Reserves::ModReserve({ rank => 'del', reserve_id => $reserve_id, branchcode => $branchcode, itemnumber => $item1->itemnumber, borrowernumber => $patron1->borrowernumber, biblionumber => $item1->biblionumber });
590
591
    $recall1->set_cancelled;
592
    is( $item1->can_be_recalled({ patron => $patron2 }), 0, "Can't recall if patron has already checked out an item attached to this biblio" );
593
594
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if on_shelf_recalls = all and items are still available" );
595
596
    Koha::CirculationRules->set_rules({
597
        branchcode => $branchcode,
598
        categorycode => $patron1->categorycode,
599
        itemtype => $item1->effective_itemtype,
600
        rules => {
601
            recalls_allowed => 1,
602
            recalls_per_record => 1,
603
            on_shelf_recalls => 'any',
604
        },
605
    });
606
    C4::Circulation::AddReturn( $item1->barcode, $branchcode );
607
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if no items are checked out" );
608
609
    C4::Circulation::AddIssue( $patron2->unblessed, $item1->barcode );
610
    is( $item1->can_be_recalled({ patron => $patron1 }), 1, "Can recall item" );
611
612
    $recall1 = Koha::Recall->new({
613
        borrowernumber => $patron1->borrowernumber,
614
        recalldate => Koha::DateUtils::dt_from_string,
615
        biblionumber => $biblio->biblionumber,
616
        branchcode => $branchcode,
617
        status => 'R',
618
        itemnumber => undef,
619
        expirationdate => undef,
620
        item_level_recall => 0
621
    })->store;
622
623
    # Patron2 has Item1 checked out. Patron1 has placed a biblio-level recall on Biblio1, so check if Item1 can fulfill Patron1's recall.
624
625
    Koha::CirculationRules->set_rules({
626
        branchcode => undef,
627
        categorycode => undef,
628
        itemtype => $item1->effective_itemtype,
629
        rules => {
630
            recalls_allowed => 0,
631
            recalls_per_record => 1,
632
            on_shelf_recalls => 'any',
633
        },
634
    });
635
    is( $item1->can_be_waiting_recall, 0, "Recalls not allowed for this itemtype" );
636
637
    Koha::CirculationRules->set_rules({
638
        branchcode => undef,
639
        categorycode => undef,
640
        itemtype => $item1->effective_itemtype,
641
        rules => {
642
            recalls_allowed => 1,
643
            recalls_per_record => 1,
644
            on_shelf_recalls => 'any',
645
        },
646
    });
647
    is( $item1->can_be_waiting_recall, 1, "Recalls are allowed for this itemtype" );
648
649
    # check_recalls tests
650
651
    $recall1 = Koha::Recall->new({
652
        borrowernumber => $patron2->borrowernumber,
653
        recalldate => Koha::DateUtils::dt_from_string,
654
        biblionumber => $biblio->biblionumber,
655
        branchcode => $branchcode,
656
        status => 'R',
657
        itemnumber => $item1->itemnumber,
658
        expirationdate => undef,
659
        item_level_recall => 1
660
    })->store;
661
    $recall2 = Koha::Recall->new({
662
        borrowernumber => $patron1->borrowernumber,
663
        recalldate => Koha::DateUtils::dt_from_string,
664
        biblionumber => $biblio->biblionumber,
665
        branchcode => $branchcode,
666
        status => 'R',
667
        itemnumber => undef,
668
        expirationdate => undef,
669
        item_level_recall => 0
670
    })->store;
671
    $recall2->set_waiting({ item => $item1 });
672
673
    # return a waiting recall
674
    my $check_recall = $item1->check_recalls;
675
    is( $check_recall->borrowernumber, $patron1->borrowernumber, "Waiting recall is highest priority and returned" );
676
677
    $recall2->revert_waiting;
678
679
    # return an item-level recall
680
    $check_recall = $item1->check_recalls;
681
    is( $check_recall->borrowernumber, $patron2->borrowernumber, "No waiting recall, so oldest item-level recall is returned" );
682
683
    $recall1->set_cancelled;
684
685
    # return a biblio-level recall
686
    $check_recall = $item1->check_recalls;
687
    is( $check_recall->borrowernumber, $patron1->borrowernumber, "No item-level recalls so oldest biblio-level recall is returned" );
688
689
    $recall2->set_cancelled;
690
};
(-)a/t/db_dependent/Koha/Patron.t (-1 / +57 lines)
Lines 19-25 Link Here
19
19
20
use Modern::Perl;
20
use Modern::Perl;
21
21
22
use Test::More tests => 3;
22
use Test::More tests => 4;
23
use Test::Exception;
23
use Test::Exception;
24
24
25
use Koha::Database;
25
use Koha::Database;
Lines 192-194 subtest 'to_api() tests' => sub { Link Here
192
192
193
    $schema->storage->txn_rollback;
193
    $schema->storage->txn_rollback;
194
};
194
};
195
196
subtest 'recalls() tests' => sub {
197
198
    plan tests => 2;
199
200
    $schema->storage->txn_begin;
201
202
    my $patron = $builder->build_object({ class => 'Koha::Patrons' });
203
    my $biblio1 = $builder->build_object({ class => 'Koha::Biblios' });
204
    my $item1 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio1->biblionumber } });
205
    my $biblio2 = $builder->build_object({ class => 'Koha::Biblios' });
206
    my $item2 = $builder->build_object({ class => 'Koha::Items' }, { value => { biblionumber => $biblio2->biblionumber } });
207
208
    Koha::Recall->new({
209
        biblionumber => $biblio1->biblionumber,
210
        borrowernumber => $patron->borrowernumber,
211
        itemnumber => $item1->itemnumber,
212
        branchcode => $patron->branchcode,
213
        recalldate => dt_from_string,
214
        status => 'R',
215
        item_level_recall => 1,
216
    })->store;
217
    Koha::Recall->new({
218
        biblionumber => $biblio2->biblionumber,
219
        borrowernumber => $patron->borrowernumber,
220
        itemnumber => $item2->itemnumber,
221
        branchcode => $patron->branchcode,
222
        recalldate => dt_from_string,
223
        status => 'R',
224
        item_level_recall => 1,
225
    })->store;
226
    Koha::Recall->new({
227
        biblionumber => $biblio1->biblionumber,
228
        borrowernumber => $patron->borrowernumber,
229
        itemnumber => undef,
230
        branchcode => $patron->branchcode,
231
        recalldate => dt_from_string,
232
        status => 'R',
233
        item_level_recall => 0,
234
    })->store;
235
    my $recall = Koha::Recall->new({
236
        biblionumber => $biblio1->biblionumber,
237
        borrowernumber => $patron->borrowernumber,
238
        itemnumber => undef,
239
        branchcode => $patron->branchcode,
240
        recalldate => dt_from_string,
241
        status => 'R',
242
        item_level_recall => 0,
243
    })->store;
244
    $recall->set_cancelled;
245
246
    is( $patron->recalls->count, 3, "Correctly gets this patron's active recalls" );
247
    is( $patron->recalls({ biblionumber => $biblio1->biblionumber })->count, 2, "Correctly gets this patron's active recalls on a specific biblio" );
248
249
    $schema->storage->txn_rollback;
250
};
(-)a/t/db_dependent/Stats.t (-1 / +1 lines)
Lines 55-61 $return_error = $@; Link Here
55
isnt ($return_error,'',"UpdateStats returns undef and croaks if type is undef");
55
isnt ($return_error,'',"UpdateStats returns undef and croaks if type is undef");
56
56
57
# returns undef and croaks if mandatory params are missing
57
# returns undef and croaks if mandatory params are missing
58
my @allowed_circulation_types = qw (renew issue localuse return);
58
my @allowed_circulation_types = qw (renew issue localuse return onsite_checkout recall);
59
my @allowed_accounts_types = qw (writeoff payment);
59
my @allowed_accounts_types = qw (writeoff payment);
60
my @circulation_mandatory_keys = qw (branch borrowernumber itemnumber ccode itemtype); #don't check type here
60
my @circulation_mandatory_keys = qw (branch borrowernumber itemnumber ccode itemtype); #don't check type here
61
my @accounts_mandatory_keys = qw (branch borrowernumber amount); #don't check type here
61
my @accounts_mandatory_keys = qw (branch borrowernumber amount); #don't check type here
(-)a/t/db_dependent/XSLT.t (-3 / +12 lines)
Lines 34-40 my $builder = t::lib::TestBuilder->new; Link Here
34
$schema->storage->txn_begin;
34
$schema->storage->txn_begin;
35
35
36
subtest 'buildKohaItemsNamespace status tests' => sub {
36
subtest 'buildKohaItemsNamespace status tests' => sub {
37
    plan tests => 13;
37
    plan tests => 14;
38
    my $itype = $builder->build_object({ class => 'Koha::ItemTypes' });
38
    my $itype = $builder->build_object({ class => 'Koha::ItemTypes' });
39
    my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes' });
39
    my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes' });
40
    my $item  = $builder->build_sample_item({ itype => $itype->itemtype });
40
    my $item  = $builder->build_sample_item({ itype => $itype->itemtype });
Lines 103-109 subtest 'buildKohaItemsNamespace status tests' => sub { Link Here
103
        }
103
        }
104
    });
104
    });
105
    $xml = C4::XSLT::buildKohaItemsNamespace( $item->biblionumber,[]);
105
    $xml = C4::XSLT::buildKohaItemsNamespace( $item->biblionumber,[]);
106
    like($xml,qr{<status>Waiting</status>},"Waiting status takes precedence over In transit");
106
    like($xml,qr{<status>Waiting</status>},"Waiting status takes precedence over In transit (holds)");
107
    $hold->cancel;
107
108
108
    $builder->build({ source => "TmpHoldsqueue", value => {
109
    $builder->build({ source => "TmpHoldsqueue", value => {
109
        itemnumber => $item->itemnumber
110
        itemnumber => $item->itemnumber
Lines 112-117 subtest 'buildKohaItemsNamespace status tests' => sub { Link Here
112
    $xml = C4::XSLT::buildKohaItemsNamespace( $item->biblionumber,[]);
113
    $xml = C4::XSLT::buildKohaItemsNamespace( $item->biblionumber,[]);
113
    like($xml,qr{<status>Pending hold</status>},"Pending status takes precedence over all");
114
    like($xml,qr{<status>Pending hold</status>},"Pending status takes precedence over all");
114
115
116
    my $recall = $builder->build_object({ class => 'Koha::Recalls', value => {
117
        biblionumber    => $item->biblionumber,
118
        itemnumber      => $item->itemnumber,
119
        branchcode      => $item->holdingbranch,
120
        status          => 'R',
121
    }});
122
    $recall->set_waiting;
123
    $xml = C4::XSLT::buildKohaItemsNamespace( $item->biblionumber,[]);
124
    like($xml,qr{<status>Waiting</status>},"Waiting status takes precedence over In transit (recalls)");
115
125
116
};
126
};
117
127
118
- 

Return to bug 19532