f726558510
Refactor the most performance critical subroutines in Circulation.pm to take objects instead of unblessed ones to reduce unnecessary unblessings and generally clean up the code. To test: 1) Ensure the following tests all pass: t/db_dependent/Circulation.t t/db_dependent/Circulation/CalcDateDue.t t/db_dependent/Circulation/CheckIfIssuedToPatron.t t/db_dependent/Circulation/GetPendingOnSiteCheckouts.t t/db_dependent/Circulation/GetTopIssues.t t/db_dependent/Circulation/IsItemIssued.t t/db_dependent/Circulation/MarkIssueReturned.t t/db_dependent/Circulation/ReturnClaims.t t/db_dependent/Circulation/Returns.t t/db_dependent/Circulation/SwitchOnSiteCheckouts.t t/db_dependent/Circulation/TooMany.t t/db_dependent/Circulation/dateexpiry.t t/db_dependent/Circulation/issue.t t/db_dependent/Circulation/maxsuspensiondays.t t/db_dependent/Circulation/transferbook.t t/db_dependent/Circulation_holdsqueue.t t/db_dependent/DecreaseLoanHighHolds.t t/db_dependent/Holds/DisallowHoldIfItemsAvailable.t t/db_dependent/Holds/RevertWaitingStatus.t t/db_dependent/ILSDI_Services.t t/db_dependent/Illrequests.t t/db_dependent/Koha/Account/Line.t t/db_dependent/Koha/Biblio.t t/db_dependent/Koha/Items.t t/db_dependent/Koha/Object.t t/db_dependent/Koha/Patrons.t t/db_dependent/Koha/Pseudonymization.t t/db_dependent/Koha/Template/Plugin/CirculationRules.t t/db_dependent/Letters/TemplateToolkit.t t/db_dependent/Members/GetAllIssues.t t/db_dependent/Members/IssueSlip.t t/db_dependent/Patron/Borrower_Discharge.t t/db_dependent/Patron/Borrower_PrevCheckout.t t/db_dependent/SIP/ILS.t t/db_dependent/Holds.t t/db_dependent/Holds/LocalHoldsPriority.t t/db_dependent/Holds/HoldFulfillmentPolicy.t t/db_dependent/Holds/HoldItemtypeLimit.t t/db_dependent/Reserves/GetReserveFee.t t/db_dependent/api/v1/return_claims.t t/db_dependent/api/v1/biblios.t t/db_dependent/api/v1/checkouts.t t/db_dependent/Reserves.t t/db_dependent/HoldsQueue.t t/db_dependent/selenium/regressions.t t/db_dependent/Koha/Plugins/Circulation_hooks.t t/db_dependent/Koha/Plugins/Recall_hooks.t t/db_dependent/Koha/Recalls.t t/db_dependent/Koha/Recall.t t/db_dependent/Circulation/_CalculateAndUpdateFine.t Sponsored-by: Gothenburg University Library Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org> Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
474 lines
11 KiB
Perl
474 lines
11 KiB
Perl
package Koha::Recall;
|
|
|
|
# Copyright 2020 Aleisha Amohia <aleisha@catalyst.net.nz>
|
|
#
|
|
# 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 <http://www.gnu.org/licenses>.
|
|
|
|
use Modern::Perl;
|
|
|
|
use Koha::Database;
|
|
use Koha::DateUtils qw( dt_from_string );
|
|
use Koha::Biblios;
|
|
use Koha::Items;
|
|
use Koha::Libraries;
|
|
use Koha::Patrons;
|
|
|
|
use base qw(Koha::Object);
|
|
|
|
=head1 NAME
|
|
|
|
Koha::Recall - Koha Recall Object class
|
|
|
|
=head1 API
|
|
|
|
=head2 Class 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->patron;
|
|
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 ) = @_;
|
|
my $library_rs = $self->_result->library;
|
|
return unless $library_rs;
|
|
return Koha::Library->_new_from_dbic( $library_rs );
|
|
}
|
|
|
|
=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->item_id });
|
|
|
|
unless ( $self->item_level ) {
|
|
# Only look at checkouts of items that are allowed to be recalled, and get the oldest one
|
|
my @items = Koha::Items->search({ biblionumber => $self->biblio_id })->as_list;
|
|
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 ) = @_;
|
|
return $self->status eq 'requested';
|
|
}
|
|
|
|
=head3 waiting
|
|
|
|
if ( $recall->waiting )
|
|
|
|
[% IF recall.waiting %]
|
|
|
|
Return true if recall is awaiting pickup.
|
|
|
|
=cut
|
|
|
|
sub waiting {
|
|
my ( $self ) = @_;
|
|
return $self->status eq 'waiting';
|
|
}
|
|
|
|
=head3 overdue
|
|
|
|
if ( $recall->overdue )
|
|
|
|
[% IF recall.overdue %]
|
|
|
|
Return true if recall is overdue to be returned.
|
|
|
|
=cut
|
|
|
|
sub overdue {
|
|
my ( $self ) = @_;
|
|
return $self->status eq 'overdue';
|
|
}
|
|
|
|
=head3 in_transit
|
|
|
|
if ( $recall->in_transit )
|
|
|
|
[% IF recall.in_transit %]
|
|
|
|
Return true if recall is in transit.
|
|
|
|
=cut
|
|
|
|
sub in_transit {
|
|
my ( $self ) = @_;
|
|
return $self->status eq 'in_transit';
|
|
}
|
|
|
|
=head3 expired
|
|
|
|
if ( $recall->expired )
|
|
|
|
[% IF recall.expired %]
|
|
|
|
Return true if recall has expired.
|
|
|
|
=cut
|
|
|
|
sub expired {
|
|
my ( $self ) = @_;
|
|
return $self->status eq 'expired';
|
|
}
|
|
|
|
=head3 cancelled
|
|
|
|
if ( $recall->cancelled )
|
|
|
|
[% IF recall.cancelled %]
|
|
|
|
Return true if recall has been cancelled.
|
|
|
|
=cut
|
|
|
|
sub cancelled {
|
|
my ( $self ) = @_;
|
|
return $self->status eq 'cancelled';
|
|
}
|
|
|
|
=head3 fulfilled
|
|
|
|
if ( $recall->fulfilled )
|
|
|
|
[% IF recall.fulfilled %]
|
|
|
|
Return true if the recall has been fulfilled.
|
|
|
|
=cut
|
|
|
|
sub fulfilled {
|
|
my ( $self ) = @_;
|
|
return $self->status eq 'fulfilled';
|
|
}
|
|
|
|
=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 ) {
|
|
$item = $self->item;
|
|
} elsif ( $self->checkout ) {
|
|
$item = $self->checkout->item;
|
|
}
|
|
|
|
my $branchcode = $self->patron->branchcode;
|
|
if ( $item ) {
|
|
$branchcode = C4::Circulation::_GetCircControlBranch( $item, $self->patron );
|
|
}
|
|
|
|
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 ) {
|
|
# already has an itemnumber
|
|
$self->update({ status => 'in_transit' });
|
|
} else {
|
|
my $itemnumber = $params->{item}->itemnumber;
|
|
$self->update({ status => 'in_transit', item_id => $itemnumber });
|
|
}
|
|
|
|
my ( $dotransfer, $messages ) = C4::Circulation::transferbook({ to_branch => $self->pickup_library_id, 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 ) {
|
|
$self->update({ status => 'requested' });
|
|
} else {
|
|
$self->update({ status => 'requested', item_id => 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 ) {
|
|
$itemnumber = $self->item_id;
|
|
$self->update({ status => 'waiting', waiting_date => dt_from_string, expiration_date => $params->{expirationdate} });
|
|
} else {
|
|
# biblio-level recall with no itemnumber. need to set itemnumber
|
|
$itemnumber = $params->{item}->itemnumber;
|
|
$self->update({ status => 'waiting', waiting_date => dt_from_string, expiration_date => $params->{expirationdate}, item_id => $itemnumber });
|
|
}
|
|
|
|
# send notice to recaller to pick up item
|
|
my $letter = C4::Letters::GetPreparedLetter(
|
|
module => 'circulation',
|
|
letter_code => 'PICKUP_RECALLED_ITEM',
|
|
branchcode => $self->pickup_library_id,
|
|
want_librarian => 0,
|
|
tables => {
|
|
biblio => $self->biblio_id,
|
|
borrowers => $self->patron_id,
|
|
items => $itemnumber,
|
|
recalls => $self->recall_id,
|
|
},
|
|
);
|
|
|
|
C4::Message->enqueue($letter, $self->patron, 'email');
|
|
|
|
return $self;
|
|
}
|
|
|
|
=head3 revert_waiting
|
|
|
|
$recall->revert_waiting;
|
|
|
|
Revert recall waiting status.
|
|
|
|
=cut
|
|
|
|
sub revert_waiting {
|
|
my ( $self ) = @_;
|
|
if ( $self->item_level ){
|
|
$self->update({ status => 'requested', waiting_date => undef });
|
|
} else {
|
|
$self->update({ status => 'requested', waiting_date => undef, item_id => 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 => 'overdue' });
|
|
C4::Log::logaction( 'RECALLS', 'OVERDUE', $self->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 => 'expired', completed => 1, completed_date => dt_from_string });
|
|
C4::Log::logaction( 'RECALLS', 'EXPIRE', $self->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 => 'cancelled', completed => 1, completed_date => dt_from_string });
|
|
C4::Log::logaction( 'RECALLS', 'CANCEL', $self->id, "Recall cancelled", 'INTRANET' ) if ( C4::Context->preference('RecallsLog') );
|
|
return $self;
|
|
}
|
|
|
|
=head3 set_fulfilled
|
|
|
|
$recall->set_fulfilled;
|
|
|
|
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_fulfilled {
|
|
my ( $self ) = @_;
|
|
$self->update({ status => 'fulfilled', completed => 1, completed_date => dt_from_string });
|
|
C4::Log::logaction( 'RECALLS', 'FILL', $self->id, "Recall fulfilled", 'INTRANET' ) if ( C4::Context->preference('RecallsLog') );
|
|
return $self;
|
|
}
|
|
|
|
=head2 Internal methods
|
|
|
|
=head3 _type
|
|
|
|
=cut
|
|
|
|
sub _type {
|
|
return 'Recall';
|
|
}
|
|
|
|
1;
|