From 711411e74475ea26c5eaf6ca6d4e023562189930 Mon Sep 17 00:00:00 2001 From: Aleisha Amohia Date: Wed, 22 Apr 2020 07:35:47 +0000 Subject: [PATCH] Bug 19532: Recalls objects and tests Koha/Recall.pm - biblio - item - patron - library - checkout - requested - waiting - overdue - in_transit - expired - cancelled - finished - calc_expirationdate - start_transfer - revert_transfer - set_waiting - revert_waiting - set_overdue - set_expired - set_cancelled - set_finished - should_be_overdue Koha/Recalls.pm - add_recall - move_recall and relevant tests Signed-off-by: David Nind Signed-off-by: David Nind Signed-off-by: Marcel de Rooy Signed-off-by: Fridolin Somers --- C4/Letters.pm | 1 + C4/Stats.pm | 2 +- Koha/Recall.pm | 476 ++++++++++++++++++++++++++++++++++ Koha/Recalls.pm | 212 +++++++++++++++ t/db_dependent/Koha/Recall.t | 190 ++++++++++++++ t/db_dependent/Koha/Recalls.t | 175 +++++++++++++ 6 files changed, 1055 insertions(+), 1 deletion(-) create mode 100644 Koha/Recall.pm create mode 100644 Koha/Recalls.pm create mode 100644 t/db_dependent/Koha/Recall.t create mode 100644 t/db_dependent/Koha/Recalls.t diff --git a/C4/Letters.pm b/C4/Letters.pm index c36553ac9a..616f9e6a5f 100644 --- a/C4/Letters.pm +++ b/C4/Letters.pm @@ -801,6 +801,7 @@ sub _parseletter_sth { ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" : ($table eq 'problem_reports') ? "SELECT * FROM $table WHERE reportid = ?" : ($table eq 'additional_contents' || $table eq 'opac_news') ? "SELECT * FROM additional_contents WHERE idnew = ?" : + ($table eq 'recalls') ? "SELECT * FROM $table WHERE recall_id = ?" : undef ; unless ($query) { warn "ERROR: No _parseletter_sth query for table '$table'"; diff --git a/C4/Stats.pm b/C4/Stats.pm index 1b883d2de1..6afb538092 100644 --- a/C4/Stats.pm +++ b/C4/Stats.pm @@ -83,7 +83,7 @@ sub UpdateStats { return () if ! defined $params; # change these arrays if new types of transaction or new parameters are allowed my @allowed_keys = qw (type branch amount other itemnumber itemtype borrowernumber ccode location); - my @allowed_circulation_types = qw (renew issue localuse return onsite_checkout); + my @allowed_circulation_types = qw (renew issue localuse return onsite_checkout recall); my @allowed_accounts_types = qw (writeoff payment); my @circulation_mandatory_keys = qw (type branch borrowernumber itemnumber ccode itemtype); my @accounts_mandatory_keys = qw (type branch borrowernumber amount); diff --git a/Koha/Recall.pm b/Koha/Recall.pm new file mode 100644 index 0000000000..b74cf6b7f0 --- /dev/null +++ b/Koha/Recall.pm @@ -0,0 +1,476 @@ +package Koha::Recall; + +# Copyright 2020 Aleisha Amohia +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Koha::Database; +use Koha::DateUtils; +use Koha::Patron; +use Koha::Biblio; +use Koha::Item; + +use base qw(Koha::Object); + +=head1 NAME + +Koha::Recall - Koha Recall Object class + +=head1 API + +=head2 Internal methods + +=cut + +=head3 biblio + + my $biblio = $recall->biblio; + +Returns the related Koha::Biblio object for this recall. + +=cut + +sub biblio { + my ( $self ) = @_; + my $biblio_rs = $self->_result->biblio; + return unless $biblio_rs; + return Koha::Biblio->_new_from_dbic( $biblio_rs ); +} + +=head3 item + + my $item = $recall->item; + +Returns the related Koha::Item object for this recall. + +=cut + +sub item { + my ( $self ) = @_; + my $item_rs = $self->_result->item; + return unless $item_rs; + return Koha::Item->_new_from_dbic( $item_rs ); +} + +=head3 patron + + my $patron = $recall->patron; + +Returns the related Koha::Patron object for this recall. + +=cut + +sub patron { + my ( $self ) = @_; + my $patron_rs = $self->_result->borrower; + return unless $patron_rs; + return Koha::Patron->_new_from_dbic( $patron_rs ); +} + +=head3 library + + my $library = $recall->library; + +Returns the related Koha::Library object for this recall. + +=cut + +sub library { + my ( $self ) = @_; + $self->{_library} = Koha::Libraries->find( $self->branchcode ); + return $self->{_library}; +} + +=head3 checkout + + my $checkout = $recall->checkout; + +Returns the related Koha::Checkout object for this recall. + +=cut + +sub checkout { + my ( $self ) = @_; + $self->{_checkout} ||= Koha::Checkouts->find({ itemnumber => $self->itemnumber }); + + unless ( $self->item_level_recall ) { + # Only look at checkouts of items that are allowed to be recalled, and get the oldest one + my @items = Koha::Items->search({ biblionumber => $self->biblionumber }); + my @itemnumbers; + foreach (@items) { + my $recalls_allowed = Koha::CirculationRules->get_effective_rule({ + branchcode => C4::Context->userenv->{'branch'}, + categorycode => $self->patron->categorycode, + itemtype => $_->effective_itemtype, + rule_name => 'recalls_allowed', + }); + if ( defined $recalls_allowed and $recalls_allowed->rule_value > 0 ) { + push ( @itemnumbers, $_->itemnumber ); + } + } + my $checkouts = Koha::Checkouts->search({ itemnumber => [ @itemnumbers ] }, { order_by => { -asc => 'date_due' } }); + $self->{_checkout} = $checkouts->next; + } + + return $self->{_checkout}; +} + +=head3 requested + + if ( $recall->requested ) + + [% IF recall.requested %] + +Return true if recall status is requested. + +=cut + +sub requested { + my ( $self ) = @_; + my $status = $self->status; + return $status && $status eq 'R'; +} + +=head3 waiting + + if ( $recall->waiting ) + + [% IF recall.waiting %] + +Return true if recall is awaiting pickup. + +=cut + +sub waiting { + my ( $self ) = @_; + my $status = $self->status; + return $status && $status eq 'W'; +} + +=head3 overdue + + if ( $recall->overdue ) + + [% IF recall.overdue %] + +Return true if recall is overdue to be returned. + +=cut + +sub overdue { + my ( $self ) = @_; + my $status = $self->status; + return $status && $status eq 'O'; +} + +=head3 in_transit + + if ( $recall->in_transit ) + + [% IF recall.in_transit %] + +Return true if recall is in transit. + +=cut + +sub in_transit { + my ( $self ) = @_; + my $status = $self->status; + return $status && $status eq 'T'; +} + +=head3 expired + + if ( $recall->expired ) + + [% IF recall.expired %] + +Return true if recall has expired. + +=cut + +sub expired { + my ( $self ) = @_; + my $status = $self->status; + return $status && $status eq 'E'; +} + +=head3 cancelled + + if ( $recall->cancelled ) + + [% IF recall.cancelled %] + +Return true if recall has been cancelled. + +=cut + +sub cancelled { + my ( $self ) = @_; + my $status = $self->status; + return $status && $status eq 'C'; +} + +=head3 finished + + if ( $recall->finished ) + + [% IF recall.finished %] + +Return true if recall is finished and has been fulfilled. + +=cut + +sub finished { + my ( $self ) = @_; + my $status = $self->status; + return $status && $status eq 'F'; +} + +=head3 calc_expirationdate + + my $expirationdate = $recall->calc_expirationdate; + $recall->update({ expirationdate => $expirationdate }); + +Calculate the expirationdate to set based on circulation rules and system preferences. + +=cut + +sub calc_expirationdate { + my ( $self ) = @_; + + my $item; + if ( $self->item_level_recall ) { + $item = $self->item; + } elsif ( $self->checkout ) { + $item = $self->checkout->item; + } + + my $branchcode = $self->patron->branchcode; + if ( $item ) { + $branchcode = C4::Circulation::_GetCircControlBranch( $item->unblessed, $self->patron->unblessed ); + } + + my $rule = Koha::CirculationRules->get_effective_rule({ + categorycode => $self->patron->categorycode, + branchcode => $branchcode, + itemtype => $item ? $item->effective_itemtype : undef, + rule_name => 'recall_shelf_time' + }); + + my $shelf_time = defined $rule ? $rule->rule_value : C4::Context->preference('RecallsMaxPickUpDelay'); + + my $expirationdate = dt_from_string->add( days => $shelf_time ); + return $expirationdate; +} + +=head3 start_transfer + + my ( $recall, $dotransfer, $messages ) = $recall->start_transfer({ item => $item_object }); + +Set the recall as in transit. + +=cut + +sub start_transfer { + my ( $self, $params ) = @_; + + if ( $self->item_level_recall ) { + # already has an itemnumber + $self->update({ status => 'T' }); + } else { + my $itemnumber = $params->{item}->itemnumber; + $self->update({ status => 'T', itemnumber => $itemnumber }); + } + + my ( $dotransfer, $messages ) = C4::Circulation::transferbook({ to_branch => $self->branchcode, from_branch => $self->item->holdingbranch, barcode => $self->item->barcode, trigger => 'Recall' }); + + return ( $self, $dotransfer, $messages ); +} + +=head3 revert_transfer + + $recall->revert_transfer; + +If a transfer is cancelled, revert the recall to requested. + +=cut + +sub revert_transfer { + my ( $self ) = @_; + + if ( $self->item_level_recall ) { + $self->update({ status => 'R' }); + } else { + $self->update({ status => 'R', itemnumber => undef }); + } + + return $self; +} + +=head3 set_waiting + + $recall->set_waiting({ + expirationdate => $expirationdate, + item => $item_object + }); + +Set the recall as waiting and update expiration date. +Notify the recall requester. + +=cut + +sub set_waiting { + my ( $self, $params ) = @_; + + my $itemnumber; + if ( $self->item_level_recall ) { + $itemnumber = $self->itemnumber; + $self->update({ status => 'W', waitingdate => dt_from_string, expirationdate => $params->{expirationdate} }); + } else { + # biblio-level recall with no itemnumber. need to set itemnumber + $itemnumber = $params->{item}->itemnumber; + $self->update({ status => 'W', waitingdate => dt_from_string, expirationdate => $params->{expirationdate}, itemnumber => $itemnumber }); + } + + # send notice to recaller to pick up item + my $letter = C4::Letters::GetPreparedLetter( + module => 'circulation', + letter_code => 'PICKUP_RECALLED_ITEM', + branchcode => $self->branchcode, + want_librarian => 0, + tables => { + biblio => $self->biblionumber, + borrowers => $self->borrowernumber, + items => $itemnumber, + recalls => $self->recall_id, + }, + ); + + C4::Message->enqueue($letter, $self->patron->unblessed, 'email'); + + return $self; +} + +=head3 revert_waiting + + $recall->revert_waiting; + +Revert recall waiting status. + +=cut + +sub revert_waiting { + my ( $self ) = @_; + if ( $self->item_level_recall ){ + $self->update({ status => 'R', waitingdate => undef }); + } else { + $self->update({ status => 'R', waitingdate => undef, itemnumber => undef }); + } + return $self; +} + +=head3 should_be_overdue + + if ( $recall->should_be_overdue ) { + $recall->set_overdue; + } + +Return true if this recall should be marked overdue + +=cut + +sub should_be_overdue { + my ( $self ) = @_; + if ( $self->requested and $self->checkout and dt_from_string( $self->checkout->date_due ) <= dt_from_string ) { + return 1; + } + return 0; +} + +=head3 set_overdue + + $recall->set_overdue; + +Set a recall as overdue when the recall has been requested and the borrower who has checked out the recalled item is late to return it. This can be done manually by the library or by cronjob. The interface is either 'INTRANET' or 'COMMANDLINE' for logging purposes. + +=cut + +sub set_overdue { + my ( $self, $params ) = @_; + my $interface = $params->{interface} || 'COMMANDLINE'; + $self->update({ status => 'O' }); + C4::Log::logaction( 'RECALLS', 'OVERDUE', $self->recall_id, "Recall status set to overdue", $interface ) if ( C4::Context->preference('RecallsLog') ); + return $self; +} + +=head3 set_expired + + $recall->set_expired({ interface => 'INTRANET' }); + +Set a recall as expired. This may be done manually or by a cronjob, either when the borrower that placed the recall takes more than RecallsMaxPickUpDelay number of days to collect their item, or if the specified expirationdate passes. The interface is either 'INTRANET' or 'COMMANDLINE' for logging purposes. + +=cut + +sub set_expired { + my ( $self, $params ) = @_; + my $interface = $params->{interface} || 'COMMANDLINE'; + $self->update({ status => 'E', old => 1, expirationdate => dt_from_string }); + C4::Log::logaction( 'RECALLS', 'EXPIRE', $self->recall_id, "Recall expired", $interface ) if ( C4::Context->preference('RecallsLog') ); + return $self; +} + +=head3 set_cancelled + + $recall->set_cancelled; + +Set a recall as cancelled. This may be done manually, either by the borrower that placed the recall, or by the library. + +=cut + +sub set_cancelled { + my ( $self ) = @_; + $self->update({ status => 'C', old => 1, cancellationdate => dt_from_string }); + C4::Log::logaction( 'RECALLS', 'CANCEL', $self->recall_id, "Recall cancelled", 'INTRANET' ) if ( C4::Context->preference('RecallsLog') ); + return $self; +} + +=head3 set_finished + + $recall->set_finished; + +Set a recall as finished. This should only be called when the item allocated to a recall is checked out to the borrower who requested the recall. + +=cut + +sub set_finished { + my ( $self ) = @_; + $self->update({ status => 'F', old => 1 }); + C4::Log::logaction( 'RECALLS', 'FULFILL', $self->recall_id, "Recall fulfilled", 'INTRANET' ) if ( C4::Context->preference('RecallsLog') ); + return $self; +} + +=head3 _type + +=cut + +sub _type { + return 'Recall'; +} + +1; diff --git a/Koha/Recalls.pm b/Koha/Recalls.pm new file mode 100644 index 0000000000..5e27d421f0 --- /dev/null +++ b/Koha/Recalls.pm @@ -0,0 +1,212 @@ +package Koha::Recalls; + +# Copyright 2020 Aleisha Amohia +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Koha::Database; +use Koha::Recall; +use Koha::DateUtils; + +use C4::Stats qw( UpdateStats ); + +use base qw(Koha::Objects); + +=head1 NAME + +Koha::Recalls - Koha Recalls Object set class + +=head1 API + +=head2 Internal methods + +=cut + +=head3 add_recall + + my ( $recall, $due_interval, $due_date ) = Koha::Recalls->add_recall({ + patron => $patron_object, + biblio => $biblio_object, + branchcode => $branchcode, + item => $item_object, + expirationdate => $expirationdate, + interface => 'OPAC', + }); + +Add a new requested recall. We assume at this point that a recall is allowed to be placed on this item or biblio. We are past the checks and are now doing the recall. +Interface param is either OPAC or INTRANET +Send a RETURN_RECALLED_ITEM notice. +Add statistics and logs. +#FIXME: Add recallnotes and priority when staff-side recalls is added + +=cut + +sub add_recall { + my ( $self, $params ) = @_; + + my $patron = $params->{patron}; + my $biblio = $params->{biblio}; + return if ( !defined($patron) or !defined($biblio) ); + my $branchcode = $params->{branchcode}; + $branchcode ||= $patron->branchcode; + my $item = $params->{item}; + my $itemnumber = $item ? $item->itemnumber : undef; + my $expirationdate = $params->{expirationdate}; + my $interface = $params->{interface}; + + if ( $expirationdate ){ + my $now = dt_from_string; + $expirationdate = dt_from_string($expirationdate)->set({ hour => $now->hour, minute => $now->minute, second => $now->second }); + } + + my $recall_request = Koha::Recall->new({ + borrowernumber => $patron->borrowernumber, + recalldate => dt_from_string(), + biblionumber => $biblio->biblionumber, + branchcode => $branchcode, + status => 'R', + itemnumber => defined $itemnumber ? $itemnumber : undef, + expirationdate => $expirationdate, + item_level_recall => defined $itemnumber ? 1 : 0, + })->store; + + if (defined $recall_request->recall_id){ # successful recall + my $recall = Koha::Recalls->find( $recall_request->recall_id ); + + # get checkout and adjust due date based on circulation rules + my $checkout = $recall->checkout; + my $recall_due_date_interval = Koha::CirculationRules->get_effective_rule({ + categorycode => $checkout->patron->categorycode, + itemtype => $checkout->item->effective_itemtype, + branchcode => $branchcode, + rule_name => 'recall_due_date_interval', + }); + my $due_interval = defined $recall_due_date_interval ? $recall_due_date_interval->rule_value : 5; + my $timestamp = dt_from_string( $recall->timestamp ); + my $due_date = $timestamp->add( days => $due_interval ); + $checkout->update({ date_due => $due_date }); + + # get itemnumber of most relevant checkout if a biblio-level recall + unless ( $recall->item_level_recall ) { $itemnumber = $checkout->itemnumber; } + + # send notice to user with recalled item checked out + my $letter = C4::Letters::GetPreparedLetter ( + module => 'circulation', + letter_code => 'RETURN_RECALLED_ITEM', + branchcode => $recall->branchcode, + tables => { + biblio => $biblio->biblionumber, + borrowers => $checkout->borrowernumber, + items => $itemnumber, + issues => $itemnumber, + }, + ); + + C4::Message->enqueue( $letter, $checkout->patron->unblessed, 'email' ); + + $item = Koha::Items->find( $itemnumber ); + # add to statistics table + UpdateStats({ + branch => C4::Context->userenv->{'branch'}, + type => 'recall', + itemnumber => $itemnumber, + borrowernumber => $recall->borrowernumber, + itemtype => $item->effective_itemtype, + ccode => $item->ccode, + }); + + # add action log + C4::Log::logaction( 'RECALLS', 'CREATE', $recall->recall_id, "Recall requested by borrower #" . $recall->borrowernumber, $interface ) if ( C4::Context->preference('RecallsLog') ); + + return ( $recall, $due_interval, $due_date ); + } + + # unable to add recall + return; +} + +=head3 move_recall + + my $message = Koha::Recalls->move_recall({ + recall_id = $recall_id, + action => $action, + itemnumber => $itemnumber, + borrowernumber => $borrowernumber, + }); + +A patron is attempting to check out an item that has been recalled by another patron. If the recall is requested/overdue, they have the option of cancelling the recall. If the recall is waiting, they also have the option of reverting the waiting status. + +We can also fulfill the recall here if the recall is placed by this borrower. + +recall_id = ID of the recall to perform the action on +action = either cancel or revert +itemnumber = itemnumber the patron is attempting to check out +borrowernumber = borrowernumber of the patron that is attemptig to check out + +=cut + +sub move_recall { + my ( $self, $params ) = @_; + + my $recall_id = $params->{recall_id}; + my $action = $params->{action}; + return 'no recall_id provided' if ( !defined $recall_id ); + my $itemnumber = $params->{itemnumber}; + my $borrowernumber = $params->{borrowernumber}; + + my $message = 'no action provided'; + + if ( $action and $action eq 'cancel' ) { + my $recall = Koha::Recalls->find( $recall_id ); + $recall->set_cancelled; + $message = 'cancelled'; + } elsif ( $action and $action eq 'revert' ) { + my $recall = Koha::Recalls->find( $recall_id ); + $recall->revert_waiting; + $message = 'reverted'; + } + + if ( $message eq 'no action provided' and $itemnumber and $borrowernumber ) { + # move_recall was not called to revert or cancel, but was called to fulfill + my $recall = Koha::Recalls->find({ borrowernumber => $borrowernumber, itemnumber => $itemnumber, old => undef }); + if ( $recall ) { + $recall->set_finished; + $message = 'fulfilled'; + } + } + + return $message; +} + +=head3 _type + +=cut + +sub _type { + return 'Recall'; +} + +=head3 object_class + +=cut + +sub object_class { + return 'Koha::Recall'; +} + +1; diff --git a/t/db_dependent/Koha/Recall.t b/t/db_dependent/Koha/Recall.t new file mode 100644 index 0000000000..e89cc88a5a --- /dev/null +++ b/t/db_dependent/Koha/Recall.t @@ -0,0 +1,190 @@ +#!/usr/bin/perl + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Test::More tests => 27; +use t::lib::TestBuilder; +use t::lib::Mocks; + +use Koha::DateUtils; + +BEGIN { + require_ok('Koha::Recall'); + require_ok('Koha::Recalls'); +} + +# Start transaction + +my $database = Koha::Database->new(); +my $schema = $database->schema(); +$schema->storage->txn_begin(); +my $dbh = C4::Context->dbh; + +my $builder = t::lib::TestBuilder->new; + +# Setup test variables + +my $item1 = $builder->build_sample_item(); +my $biblio1 = $item1->biblio; +my $branch1 = $item1->holdingbranch; +my $itemtype1 = $item1->effective_itemtype; + +my $item2 = $builder->build_sample_item(); +my $biblio2 = $item2->biblio; +my $branch2 = $item2->holdingbranch; +my $itemtype2 = $item2->effective_itemtype; + +my $category1 = $builder->build({ source => 'Category' })->{ categorycode }; +my $patron1 = $builder->build_object({ class => 'Koha::Patrons', value => { categorycode => $category1, branchcode => $branch1 } }); +my $patron2 = $builder->build_object({ class => 'Koha::Patrons', value => { categorycode => $category1, branchcode => $branch1 } }); +t::lib::Mocks::mock_userenv({ patron => $patron1 }); +my $old_recalls_count = Koha::Recalls->search({ old => 1 })->count; + +Koha::CirculationRules->set_rule({ + branchcode => undef, + categorycode => undef, + itemtype => undef, + rule_name => 'recalls_allowed', + rule_value => '10', +}); + +my $overdue_date = dt_from_string->subtract( days => 4 ); +C4::Circulation::AddIssue( $patron2->unblessed, $item1->barcode, $overdue_date ); + +my $recall1 = Koha::Recall->new({ + borrowernumber => $patron1->borrowernumber, + recalldate => dt_from_string, + biblionumber => $biblio1->biblionumber, + branchcode => $branch1, + status => 'R', + itemnumber => $item1->itemnumber, + expirationdate => undef, + item_level_recall => 1 +})->store; + +is( $recall1->biblio->title, $biblio1->title, "Recall biblio relationship correctly linked" ); +is( $recall1->item->homebranch, $item1->homebranch, "Recall item relationship correctly linked" ); +is( $recall1->patron->categorycode, $category1, "Recall patron relationship correctly linked" ); +is( $recall1->library->branchname, Koha::Libraries->find( $branch1 )->branchname, "Recall library relationship correctly linked" ); +is( $recall1->checkout->itemnumber, $item1->itemnumber, "Recall checkout relationship correctly linked" ); +is( $recall1->requested, 1, "Recall has been requested" ); + +is( $recall1->should_be_overdue, 1, "Correctly calculated that recall should be marked overdue" ); +$recall1->set_overdue({ interface => 'COMMANDLINE' }); +is( $recall1->overdue, 1, "Recall is overdue" ); + +$recall1->set_cancelled; +is( $recall1->cancelled, 1, "Recall is cancelled" ); + +my $recall2 = Koha::Recall->new({ + borrowernumber => $patron1->borrowernumber, + recalldate => dt_from_string, + biblionumber => $biblio1->biblionumber, + branchcode => $branch1, + status => 'R', + itemnumber => $item1->itemnumber, + expirationdate => undef, + item_level_recall => 1 +})->store; + +Koha::CirculationRules->set_rule({ + branchcode => undef, + categorycode => undef, + itemtype => undef, + rule_name => 'recall_shelf_time', + rule_value => undef, +}); + +t::lib::Mocks::mock_preference( 'RecallsMaxPickUpDelay', 7 ); +my $expected_expirationdate = dt_from_string->add({ days => 7 }); +my $expirationdate = $recall2->calc_expirationdate; +is( $expirationdate, $expected_expirationdate, "Expiration date calculated based on system preference as no circulation rules are set" ); + +Koha::CirculationRules->set_rule({ + branchcode => undef, + categorycode => undef, + itemtype => undef, + rule_name => 'recall_shelf_time', + rule_value => '3', +}); +$expected_expirationdate = dt_from_string->add({ days => 3 }); +$expirationdate = $recall2->calc_expirationdate; +is( $expirationdate, $expected_expirationdate, "Expiration date calculated based on circulation rules" ); + +$recall2->set_waiting({ expirationdate => $expirationdate }); +is( $recall2->waiting, 1, "Recall is waiting" ); + +my $notice = C4::Message->find_last_message( $patron1->unblessed, 'PICKUP_RECALLED_ITEM', 'email' ); +ok( defined $notice, "Patron was notified to pick up waiting recall" ); + +$recall2->set_expired({ interface => 'COMMANDLINE' }); +is( $recall2->expired, 1, "Recall has expired" ); + +my $old_recalls_count_now = Koha::Recalls->search({ old => 1 })->count; +is( $old_recalls_count_now, $old_recalls_count + 2, "Recalls have been flagged as old when cancelled or expired" ); + +my $recall3 = Koha::Recall->new({ + borrowernumber => $patron1->borrowernumber, + recalldate => dt_from_string, + biblionumber => $biblio1->biblionumber, + branchcode => $branch1, + status => 'R', + itemnumber => $item1->itemnumber, + expirationdate => undef, + item_level_recall => 1 +})->store; + +# test that recall gets T status +$recall3->start_transfer; +is( $recall3->in_transit, 1, "Recall is in transit" ); + +$recall3->revert_transfer; +is( $recall3->requested, 1, "Recall transfer has been cancelled and the status reverted" ); +is( $recall3->itemnumber, $item1->itemnumber, "Item persists for item-level recall" ); + +# for testing purposes, pretend the item gets checked out +$recall3->set_finished; +is( $recall3->finished, 1, "Recall has been fulfilled" ); + +C4::Circulation::AddIssue( $patron2->unblessed, $item1->barcode ); +my $recall4 = Koha::Recall->new({ + borrowernumber => $patron1->borrowernumber, + recalldate => dt_from_string, + biblionumber => $biblio1->biblionumber, + branchcode => $branch1, + status => 'R', + itemnumber => undef, + expirationdate => undef, + item_level_recall => 0, +})->store; + +ok( !defined $recall4->item, "No relevant item returned for a biblio-level recall" ); +is( $recall4->checkout->itemnumber, $item1->itemnumber, "Return most relevant checkout for a biblio-level recall"); + +$recall4->set_waiting({ item => $item1, expirationdate => $expirationdate }); +is( $recall4->itemnumber, $item1->itemnumber, "Item has been allocated to biblio-level recall" ); + +$recall4->revert_waiting; +ok( !defined $recall4->itemnumber, "Itemnumber has been removed from biblio-level recall when reverting waiting status" ); + +$recall4->start_transfer({ item => $item1 }); +is( $recall4->itemnumber, $item1->itemnumber, "Itemnumber saved to recall when item is transferred" ); +$recall4->revert_transfer; +ok( !defined $recall4->itemnumber, "Itemnumber has been removed from biblio-level recall when reverting transfer status" ); + +$schema->storage->txn_rollback(); diff --git a/t/db_dependent/Koha/Recalls.t b/t/db_dependent/Koha/Recalls.t new file mode 100644 index 0000000000..07f8d9f1ad --- /dev/null +++ b/t/db_dependent/Koha/Recalls.t @@ -0,0 +1,175 @@ +#!/usr/bin/perl + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Test::More tests => 19; +use t::lib::TestBuilder; +use t::lib::Mocks; + +use Koha::DateUtils; + +BEGIN { + require_ok('Koha::Recall'); + require_ok('Koha::Recalls'); +} + +# Start transaction + +my $database = Koha::Database->new(); +my $schema = $database->schema(); +$schema->storage->txn_begin(); +my $dbh = C4::Context->dbh; + +my $builder = t::lib::TestBuilder->new; + +# Setup test variables + +my $item1 = $builder->build_sample_item(); +my $biblio1 = $item1->biblio; +my $branch1 = $item1->holdingbranch; +my $itemtype1 = $item1->effective_itemtype; +my $item2 = $builder->build_sample_item(); +my $biblio2 = $item1->biblio; +my $branch2 = $item1->holdingbranch; +my $itemtype2 = $item1->effective_itemtype; + +my $category1 = $builder->build({ source => 'Category' })->{ categorycode }; +my $patron1 = $builder->build_object({ class => 'Koha::Patrons', value => { categorycode => $category1, branchcode => $branch1 } }); +my $patron2 = $builder->build_object({ class => 'Koha::Patrons', value => { categorycode => $category1, branchcode => $branch2 } }); +my $patron3 = $builder->build_object({ class => 'Koha::Patrons', value => { categorycode => $category1, branchcode => $branch1 } }); +t::lib::Mocks::mock_userenv({ patron => $patron1 }); + +Koha::CirculationRules->set_rules({ + branchcode => undef, + categorycode => undef, + itemtype => undef, + rules => { + 'recall_due_date_interval' => undef, + 'recalls_allowed' => 10, + } +}); + +C4::Circulation::AddIssue( $patron3->unblessed, $item1->barcode ); +C4::Circulation::AddIssue( $patron3->unblessed, $item2->barcode ); + +my ( $recall, $due_interval, $due_date ) = Koha::Recalls->add_recall({ + patron => undef, + biblio => $biblio1, + branchcode => $branch1, + item => $item1, + expirationdate => undef, + interface => 'COMMANDLINE', +}); +ok( !defined $recall, "Can't add a recall without specifying a patron" ); + +( $recall, $due_interval, $due_date ) = Koha::Recalls->add_recall({ + patron => $patron1, + biblio => undef, + branchcode => $branch1, + item => $item1, + expirationdate => undef, + interface => 'COMMANDLINE', +}); +ok( !defined $recall, "Can't add a recall without specifying a biblio" ); + +( $recall, $due_interval, $due_date ) = Koha::Recalls->add_recall({ + patron => $patron1, + biblio => undef, + branchcode => $branch1, + item => $item1, + expirationdate => undef, + interface => 'COMMANDLINE', +}); +ok( !defined $recall, "Can't add a recall without specifying a biblio" ); + +( $recall, $due_interval, $due_date ) = Koha::Recalls->add_recall({ + patron => $patron2, + biblio => $biblio1, + branchcode => undef, + item => $item2, + expirationdate => undef, + interface => 'COMMANDLINE', +}); +is( $recall->branchcode, $branch2, "No pickup branch specified so patron branch used" ); +is( $due_interval, 5, "Recall due date interval defaults to 5 if not specified" ); + +Koha::CirculationRules->set_rule({ + branchcode => undef, + categorycode => undef, + itemtype => undef, + rule_name => 'recall_due_date_interval', + rule_value => 3, +}); +( $recall, $due_interval, $due_date ) = Koha::Recalls->add_recall({ + patron => $patron1, + biblio => $biblio1, + branchcode => undef, + item => $item1, + expirationdate => undef, + interface => 'COMMANDLINE', +}); +is( $due_interval, 3, "Recall due date interval is based on circulation rules" ); + +( $recall, $due_interval, $due_date ) = Koha::Recalls->add_recall({ + patron => $patron1, + biblio => $biblio1, + branchcode => $branch1, + item => undef, + expirationdate => undef, + interface => 'COMMANDLINE', +}); +is( $recall->item_level_recall, 0, "No item provided so recall not flagged as item-level" ); + +my $expected_due_date = dt_from_string->add( days => 3 ); +is( dt_from_string( $recall->checkout->date_due ), $expected_due_date, "Checkout due date has correctly been extended by recall_due_date_interval days" ); +is( $due_date, $expected_due_date, "Due date correctly returned" ); + +my $messages_count = Koha::Notice::Messages->search({ borrowernumber => $patron3->borrowernumber, letter_code => 'RETURN_RECALLED_ITEM' })->count; +is( $messages_count, 3, "RETURN_RECALLED_ITEM notice successfully sent to checkout borrower" ); + +my $message = Koha::Recalls->move_recall; +is( $message, 'no recall_id provided', "Can't move a recall without specifying which recall" ); + +$message = Koha::Recalls->move_recall({ recall_id => $recall->recall_id }); +is( $message, 'no action provided', "No clear action to perform on recall" ); +$message = Koha::Recalls->move_recall({ recall_id => $recall->recall_id, action => 'whatever' }); +is( $message, 'no action provided', "Legal action not provided to perform on recall" ); + +$recall->set_waiting({ item => $item1 }); +ok( $recall->waiting, "Recall is waiting" ); +Koha::Recalls->move_recall({ recall_id => $recall->recall_id, action => 'revert' }); +$recall = Koha::Recalls->find( $recall->recall_id ); +ok( $recall->requested, "Recall reverted to requested with move_recall" ); + +Koha::Recalls->move_recall({ recall_id => $recall->recall_id, action => 'cancel' }); +$recall = Koha::Recalls->find( $recall->recall_id ); +ok( $recall->cancelled, "Recall cancelled with move_recall" ); + +( $recall, $due_interval, $due_date ) = Koha::Recalls->add_recall({ + patron => $patron1, + biblio => $biblio1, + branchcode => $branch1, + item => $item2, + expirationdate => undef, + interface => 'COMMANDLINE', +}); +$message = Koha::Recalls->move_recall({ recall_id => $recall->recall_id, itemnumber => $item2->itemnumber, borrowernumber => $patron1->borrowernumber }); +$recall = Koha::Recalls->find( $recall->recall_id ); +ok( $recall->finished, "Recall fulfilled with move_recall" ); + +$schema->storage->txn_rollback(); -- 2.39.5