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

(-)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
823
824
=cut
825
826
sub recalls {
827
    my ( $self ) = @_;
828
    $self->{_recalls} ||= Koha::Recalls->search({ biblionumber => $self->biblionumber, old => undef });
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 (+145 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->first;
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_set_waiting_recall
949
950
    if ( $item->can_set_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_set_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
}
845
=head3 _type
990
=head3 _type
846
991
847
=cut
992
=cut
(-)a/Koha/Patron.pm (+21 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
    if ( $biblionumber ) {
1731
        return Koha::Recalls->search({ borrowernumber => $self->borrowernumber, old => undef, biblionumber => $biblionumber });
1732
    }
1733
    return Koha::Recalls->search({ borrowernumber => $self->borrowernumber, old => undef });
1734
}
1735
1715
=head2 Internal methods
1736
=head2 Internal methods
1716
1737
1717
=head3 _type
1738
=head3 _type
(-)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({ location => '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 / +107 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 => 12;
500
501
    $schema->storage->txn_begin;
502
    my $item1 = $builder->build_sample_item;
503
    my $biblio = $item1->biblio;
504
    my $branchcode = $item1->holdingbranch;
505
    my $patron1 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } });
506
    my $patron2 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } });
507
    my $patron3 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } });
508
    my $item2 = $builder->build_object({ class => 'Koha::Items', value => { holdingbranch => $branchcode, homebranch => $branchcode, biblionumber => $biblio->biblionumber, itype => $item1->effective_itemtype } });
509
    t::lib::Mocks::mock_userenv({ patron => $patron1 });
510
511
    my $recall1 = Koha::Recall->new({
512
        borrowernumber => $patron1->borrowernumber,
513
        recalldate => Koha::DateUtils::dt_from_string,
514
        biblionumber => $biblio->biblionumber,
515
        branchcode => $branchcode,
516
        status => 'R',
517
        itemnumber => $item1->itemnumber,
518
        expirationdate => undef,
519
        item_level_recall => 1
520
    })->store;
521
    my $recall2 = Koha::Recall->new({
522
        borrowernumber => $patron2->borrowernumber,
523
        recalldate => Koha::DateUtils::dt_from_string,
524
        biblionumber => $biblio->biblionumber,
525
        branchcode => $branchcode,
526
        status => 'R',
527
        itemnumber => $item1->itemnumber,
528
        expirationdate => undef,
529
        item_level_recall => 0
530
    })->store;
531
532
    is( $item1->recall->borrowernumber, $patron1->borrowernumber, 'Correctly returns oldest recall' );
533
534
    $recall2->set_cancelled;
535
536
    t::lib::Mocks::mock_preference('UseRecalls', 0);
537
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall with UseRecalls disabled" );
538
539
    t::lib::Mocks::mock_preference("UseRecalls", 1);
540
541
    $item1->update({ notforloan => 1 });
542
    is( $item1->can_be_recalled({ patron => $patron1 }), 0 "Can't recall that is not for loan" );
543
    $item1->update({ notforloan => 0, itemlost => 1 });
544
    is( $item1->can_be_recalled({ patron => $patron1 }), 0 "Can't recall that is marked lost" );
545
    $item1->update({ itemlost => 0, withdrawn => 1 });
546
    is( $item1->can_be_recalled({ patron => $patron1 }), 0 "Can't recall that is withdrawn" );
547
    is( $item1->can_be_recalled({ patron => $patron1 }), 0 "Can't recall item if not checked out" );
548
549
    $item1->update({ withdrawn => 0 });
550
    C4::Circulation::AddIssue( $patron2->unblessed, $item1->barcode );
551
552
    Koha::CirculationRules->set_rules({
553
        branchcode => $branchcode,
554
        categorycode => $patron1->categorycode,
555
        itemtype => $item1->effective_itemtype,
556
        rules => {
557
            recalls_allowed => 0,
558
            recalls_per_record => 1,
559
            on_shelf_recalls => 'all',
560
        },
561
    });
562
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if recalls_allowed = 0" );
563
564
    Koha::CirculationRules->set_rules({
565
        branchcode => $branchcode,
566
        categorycode => $patron1->categorycode,
567
        itemtype => $item1->effective_itemtype,
568
        rules => {
569
            recalls_allowed => 1,
570
            recalls_per_record => 1,
571
            on_shelf_recalls => 'all',
572
        },
573
    });
574
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has more existing recall(s) than recalls_allowed" );
575
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has more existing recall(s) than recalls_per_record" );
576
577
    $recall1->set_cancelled;
578
    is( $item1->can_be_recalled({ patron => $patron2 }), 0, "Can't recall if patron has already checked out an item attached to this biblio" );
579
580
    is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if on_shelf_recalls = all and items are still available" );
581
582
    Koha::CirculationRules->set_rules({
583
        branchcode => $branchcode,
584
        categorycode => $patron1->categorycode,
585
        itemtype => $item1->effective_itemtype,
586
        rules => {
587
            recalls_allowed => 1,
588
            recalls_per_record => 1,
589
            on_shelf_recalls => 'any',
590
        },
591
    });
592
    C4::Circulation::AddReturn( $item2->barcode, $branchcode );
593
    is( $biblio->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if no items are checked out" );
594
595
    $recall2->set_cancelled;
596
597
    $item1->update({ withdrawn => 1 });
598
    is( $item1->can_be_recalled({ patron => $patron1 }), 1, "Can recall one item" );
599
600
    $schema->storage->txn_rollback;
601
};

Return to bug 19532