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

(-)a/Koha/Exceptions.pm (+5 lines)
Lines 15-20 use Exception::Class ( Link Here
15
        isa => 'Koha::Exception',
15
        isa => 'Koha::Exception',
16
        description => 'Same object already exists',
16
        description => 'Same object already exists',
17
    },
17
    },
18
    'Koha::Exceptions::InvalidStatus' => {
19
        isa         => 'Koha::Exception',
20
        description => 'The current status is not valid in context',
21
        fields      => ['invalid_status'],
22
    },
18
    'Koha::Exceptions::ObjectNotFound' => {
23
    'Koha::Exceptions::ObjectNotFound' => {
19
        isa => 'Koha::Exception',
24
        isa => 'Koha::Exception',
20
        description => 'The required object doesn\'t exist',
25
        description => 'The required object doesn\'t exist',
(-)a/Koha/Hold.pm (+116 lines)
Lines 30-35 use Koha::AuthorisedValues; Link Here
30
use Koha::DateUtils qw( dt_from_string );
30
use Koha::DateUtils qw( dt_from_string );
31
use Koha::Patrons;
31
use Koha::Patrons;
32
use Koha::Biblios;
32
use Koha::Biblios;
33
use Koha::Hold::CancellationRequests;
33
use Koha::Items;
34
use Koha::Items;
34
use Koha::Libraries;
35
use Koha::Libraries;
35
use Koha::Old::Holds;
36
use Koha::Old::Holds;
Lines 38-43 use Koha::Plugins; Link Here
38
39
39
use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
40
use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
40
41
42
use Koha::Exceptions;
41
use Koha::Exceptions::Hold;
43
use Koha::Exceptions::Hold;
42
44
43
use base qw(Koha::Object);
45
use base qw(Koha::Object);
Lines 416-421 sub is_cancelable_from_opac { Link Here
416
    return 0; # if ->is_in_transit or if ->is_waiting or ->is_in_processing
418
    return 0; # if ->is_in_transit or if ->is_waiting or ->is_in_processing
417
}
419
}
418
420
421
=head3 cancellation_requestable_from_opac
422
423
    if ( $hold->cancellation_requestable_from_opac ) { ... }
424
425
Returns a I<boolean> representing if a cancellation request can be placed on the hold
426
from the OPAC. It targets holds that cannot be cancelled from the OPAC (see the
427
B<is_cancelable_from_opac> method above), but for which circulation rules allow
428
requesting cancellation.
429
430
Throws a B<Koha::Exceptions::InvalidStatus> exception with the following I<invalid_status>
431
values:
432
433
=over 4
434
435
=item B<'hold_not_waiting'>: the hold is expected to be waiting and it is not.
436
437
=item B<'no_item_linked'>: the waiting hold doesn't have an item properly linked.
438
439
=back
440
441
=cut
442
443
sub cancellation_requestable_from_opac {
444
    my ( $self ) = @_;
445
446
    Koha::Exceptions::InvalidStatus->throw( invalid_status => 'hold_not_waiting' )
447
      unless $self->is_waiting;
448
449
    my $item = $self->item;
450
451
    Koha::Exceptions::InvalidStatus->throw( invalid_status => 'no_item_linked' )
452
      unless $item;
453
454
    my $patron = $self->patron;
455
456
    my $controlbranch = $patron->branchcode;
457
458
    if ( C4::Context->preference('ReservesControlBranch') eq 'ItemHomeLibrary' ) {
459
        $controlbranch = $item->homebranch;
460
    }
461
462
    return Koha::CirculationRules->get_effective_rule(
463
        {
464
            categorycode => $patron->categorycode,
465
            itemtype     => $item->itype,
466
            branchcode   => $controlbranch,
467
            rule_name    => 'waiting_hold_cancellation',
468
        }
469
    )->rule_value ? 1 : 0;
470
}
471
472
=head3 owner_cancellation_request
473
474
    if ( $hold->owner_cancellation_request ) { ... }
475
476
Returns a I<Koha::Hold::CancellationRequest> if the owner has placed one.
477
478
=cut
479
480
sub owner_cancellation_request {
481
    my ($self) = @_;
482
483
    my ($owner_request) = $self->cancellation_requests->search(
484
        { requester_id => $self->borrowernumber } )->as_list;
485
486
    return $owner_request;
487
}
488
419
=head3 is_at_destination
489
=head3 is_at_destination
420
490
421
Returns true if hold is waiting
491
Returns true if hold is waiting
Lines 525-530 sub is_suspended { Link Here
525
    return $self->suspend();
595
    return $self->suspend();
526
}
596
}
527
597
598
=head3 add_cancellation_request
599
600
    my $cancellation_request = $hold->add_cancellation_request(
601
        {
602
          [ requester_id  => $patron->id,
603
            creation_date => $creation_date,
604
            source        => 'owner', ]
605
        }
606
    );
607
608
Adds a cancellation request to the hold. Returns the generated
609
I<Koha::Hold::CancellationRequest> object.
610
611
=cut
612
613
sub add_cancellation_request {
614
    my ( $self, $params ) = @_;
615
616
    my $request = Koha::Hold::CancellationRequest->new(
617
        {   hold_id      => $self->id,
618
            requester_id => $params->{requester_id},
619
            ( $params->{creation_date} ? ( creation_date => $params->{creation_date} ) : () ),
620
            source => $params->{source},
621
        }
622
    )->store;
623
624
    $request->discard_changes;
625
626
    return $request;
627
}
628
629
=head3 cancellation_requests
630
631
    my $cancellation_requests = $hold->cancellation_requests;
632
633
Returns related a I<Koha::Hold::CancellationRequests> resultset.
634
635
=cut
636
637
sub cancellation_requests {
638
    my ($self) = @_;
639
640
    return Koha::Hold::CancellationRequests->search( { hold_id => $self->id } );
641
}
528
642
529
=head3 cancel
643
=head3 cancel
530
644
Lines 584-589 sub cancel { Link Here
584
                }
698
                }
585
            }
699
            }
586
700
701
            $self->cancellation_requests->filter_by_current->accept;
702
587
            my $old_me = $self->_move_to_old;
703
            my $old_me = $self->_move_to_old;
588
704
589
            Koha::Plugins->call(
705
            Koha::Plugins->call(
(-)a/t/db_dependent/Koha/Hold.t (-3 / +239 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 => 8;
23
23
24
use Test::Exception;
24
use Test::Exception;
25
use Test::MockModule;
25
use Test::MockModule;
Lines 495-501 subtest 'is_pickup_location_valid() tests' => sub { Link Here
495
495
496
subtest 'cancel() tests' => sub {
496
subtest 'cancel() tests' => sub {
497
497
498
    plan tests => 6;
498
    plan tests => 7;
499
499
500
    $schema->storage->txn_begin;
500
    $schema->storage->txn_begin;
501
501
Lines 614-619 subtest 'cancel() tests' => sub { Link Here
614
        )->cancel({ skip_holds_queue => 0 });
614
        )->cancel({ skip_holds_queue => 0 });
615
    };
615
    };
616
616
617
    subtest 'cancellation_requests handling' => sub {
618
619
        plan tests => 2;
620
621
        my $manager = $builder->build_object({ class => 'Koha::Patrons' });
622
623
        t::lib::Mocks::mock_userenv({ patron => $manager, branchcode => $manager->branchcode });
624
625
        my $item = $builder->build_sample_item;
626
        my $hold = $builder->build_object(
627
            {
628
                class => 'Koha::Holds',
629
                value => {
630
                    biblionumber => $item->biblionumber,
631
                    itemnumber   => $item->itemnumber,
632
                    found        => 'W',
633
                }
634
            }
635
        );
636
637
        my $request = $hold->add_cancellation_request(
638
            {
639
                requester_id  => $hold->borrowernumber,
640
            }
641
        );
642
643
        # limit noise
644
        t::lib::Mocks::mock_preference( 'ExpireReservesMaxPickUpDelayCharge', undef );
645
        t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
646
647
        is( $request->status, 'requested' );
648
649
        $hold->cancel;
650
651
        $request->discard_changes;
652
653
        is( $request->status, 'accepted', 'When holds are cancellaed, cancellation requests are implicitly accepted' );
654
    };
655
617
    $schema->storage->txn_rollback;
656
    $schema->storage->txn_rollback;
618
};
657
};
619
658
Lines 656-658 subtest 'suspend_hold() and resume() tests' => sub { Link Here
656
695
657
    $schema->storage->txn_rollback;
696
    $schema->storage->txn_rollback;
658
};
697
};
659
- 
698
699
subtest 'cancellation_requests() and add_cancellation_request() tests' => sub {
700
701
    plan tests => 8;
702
703
    $schema->storage->txn_begin;
704
705
    t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
706
707
    my $hold = $builder->build_object( { class => 'Koha::Holds', } );
708
709
    is( $hold->cancellation_requests->count, 0 );
710
711
    # Add two cancellation requests
712
    my $request_1 = $hold->add_cancellation_request;
713
714
    is( $request_1->requester_id, undef );
715
    is( $request_1->status, 'requested', 'Default value set' );
716
    isnt( $request_1->creation_date, undef );
717
718
    my $requester     = $builder->build_object( { class => 'Koha::Patrons' } );
719
    my $creation_date = '2021-06-25 14:05:35';
720
721
    my $request_2 = $hold->add_cancellation_request(
722
        {
723
            requester_id  => $requester->id,
724
            creation_date => $creation_date,
725
        }
726
    );
727
728
    is( $request_2->requester_id, $requester->id, 'Passed requester_id set' );
729
    is( $request_2->creation_date, $creation_date, 'Passed creation_date set' );
730
731
    is( $hold->cancellation_requests->count, 2 );
732
733
    subtest 'owner_cancellation_request() tests' => sub {
734
735
        plan tests => 3;
736
737
        my $owner_request = $hold->owner_cancellation_request;
738
739
        ok( !$owner_request );
740
741
        $hold->add_cancellation_request({ requester_id => $hold->borrowernumber });
742
743
        $owner_request = $hold->owner_cancellation_request;
744
745
        is( ref($owner_request), 'Koha::Hold::CancellationRequest' );
746
        is( $owner_request->requester_id, $hold->borrowernumber );
747
    };
748
749
    $schema->storage->txn_rollback;
750
};
751
752
subtest 'cancellation_requestable_from_opac() tests' => sub {
753
754
    plan tests => 5;
755
756
    $schema->storage->txn_begin;
757
758
    my $category =
759
      $builder->build_object( { class => 'Koha::Patron::Categories' } );
760
    my $item_home_library =
761
      $builder->build_object( { class => 'Koha::Libraries' } );
762
    my $patron_home_library =
763
      $builder->build_object( { class => 'Koha::Libraries' } );
764
765
    my $item =
766
      $builder->build_sample_item( { library => $item_home_library->id } );
767
    my $patron = $builder->build_object(
768
        {
769
            class => 'Koha::Patrons',
770
            value => { branchcode => $patron_home_library->id }
771
        }
772
    );
773
774
    subtest 'Exception cases' => sub {
775
776
        plan tests => 4;
777
778
        my $hold = $builder->build_object(
779
            {
780
                class => 'Koha::Holds',
781
                value => {
782
                    itemnumber     => undef,
783
                    found          => undef,
784
                    borrowernumber => $patron->id
785
                }
786
            }
787
        );
788
789
        throws_ok { $hold->cancellation_requestable_from_opac; }
790
        'Koha::Exceptions::InvalidStatus',
791
          'Exception thrown because hold is not waiting';
792
793
        is( $@->invalid_status, 'hold_not_waiting' );
794
795
        $hold = $builder->build_object(
796
            {
797
                class => 'Koha::Holds',
798
                value => {
799
                    itemnumber     => undef,
800
                    found          => 'W',
801
                    borrowernumber => $patron->id
802
                }
803
            }
804
        );
805
806
        throws_ok { $hold->cancellation_requestable_from_opac; }
807
        'Koha::Exceptions::InvalidStatus',
808
          'Exception thrown because waiting hold has no item linked';
809
810
        is( $@->invalid_status, 'no_item_linked' );
811
    };
812
813
    # set default rule to enabled
814
    Koha::CirculationRules->set_rule(
815
        {
816
            categorycode => '*',
817
            itemtype     => '*',
818
            branchcode   => '*',
819
            rule_name    => 'waiting_hold_cancellation',
820
            rule_value   => 1,
821
        }
822
    );
823
824
    my $hold = $builder->build_object(
825
        {
826
            class => 'Koha::Holds',
827
            value => {
828
                itemnumber     => $item->id,
829
                found          => 'W',
830
                borrowernumber => $patron->id
831
            }
832
        }
833
    );
834
835
    t::lib::Mocks::mock_preference( 'ReservesControlBranch',
836
        'ItemHomeLibrary' );
837
838
    Koha::CirculationRules->set_rule(
839
        {
840
            categorycode => $patron->categorycode,
841
            itemtype     => $item->itype,
842
            branchcode   => $item->homebranch,
843
            rule_name    => 'waiting_hold_cancellation',
844
            rule_value   => 0,
845
        }
846
    );
847
848
    ok( !$hold->cancellation_requestable_from_opac );
849
850
    Koha::CirculationRules->set_rule(
851
        {
852
            categorycode => $patron->categorycode,
853
            itemtype     => $item->itype,
854
            branchcode   => $item->homebranch,
855
            rule_name    => 'waiting_hold_cancellation',
856
            rule_value   => 1,
857
        }
858
    );
859
860
    ok(
861
        $hold->cancellation_requestable_from_opac,
862
        'Make sure it is picking the right circulation rule'
863
    );
864
865
    t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
866
867
    Koha::CirculationRules->set_rule(
868
        {
869
            categorycode => $patron->categorycode,
870
            itemtype     => $item->itype,
871
            branchcode   => $patron->branchcode,
872
            rule_name    => 'waiting_hold_cancellation',
873
            rule_value   => 0,
874
        }
875
    );
876
877
    ok( !$hold->cancellation_requestable_from_opac );
878
879
    Koha::CirculationRules->set_rule(
880
        {
881
            categorycode => $patron->categorycode,
882
            itemtype     => $item->itype,
883
            branchcode   => $patron->branchcode,
884
            rule_name    => 'waiting_hold_cancellation',
885
            rule_value   => 1,
886
        }
887
    );
888
889
    ok(
890
        $hold->cancellation_requestable_from_opac,
891
        'Make sure it is picking the right circulation rule'
892
    );
893
894
    $schema->storage->txn_rollback;
895
};

Return to bug 22456