409 lines
12 KiB
Perl
409 lines
12 KiB
Perl
package Koha::Checkout;
|
||
|
||
# Copyright ByWater Solutions 2015
|
||
# Copyright 2016 Koha Development Team
|
||
#
|
||
# 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 <https://www.gnu.org/licenses>.
|
||
|
||
use Modern::Perl;
|
||
|
||
use DateTime;
|
||
use Try::Tiny qw( catch try );
|
||
|
||
use C4::Circulation qw( AddRenewal CanBookBeRenewed LostItem MarkIssueReturned );
|
||
use Koha::Booking;
|
||
use Koha::Checkouts::Renewals;
|
||
use Koha::Checkouts::ReturnClaims;
|
||
use Koha::Database;
|
||
use Koha::DateUtils qw( dt_from_string );
|
||
use Koha::Items;
|
||
use Koha::Libraries;
|
||
|
||
use base qw(Koha::Object);
|
||
|
||
=head1 NAME
|
||
|
||
Koha::Checkout - Koha Checkout object class
|
||
|
||
=head1 API
|
||
|
||
=head2 Class methods
|
||
|
||
=cut
|
||
|
||
=head3 is_overdue
|
||
|
||
my $is_overdue = $checkout->is_overdue( [ $reference_dt ] );
|
||
|
||
Return 1 if the checkout is overdue.
|
||
|
||
A reference date can be passed, in this case it will be used, otherwise today
|
||
will be the reference date.
|
||
|
||
=cut
|
||
|
||
sub is_overdue {
|
||
my ( $self, $dt ) = @_;
|
||
$dt ||= dt_from_string();
|
||
|
||
my $is_overdue =
|
||
DateTime->compare( dt_from_string( $self->date_due, 'sql' ), $dt ) == -1
|
||
? 1
|
||
: 0;
|
||
return $is_overdue;
|
||
}
|
||
|
||
=head3 item
|
||
|
||
my $item = $checkout->item;
|
||
|
||
Return the checked out item
|
||
|
||
=cut
|
||
|
||
sub item {
|
||
my ($self) = @_;
|
||
my $item_rs = $self->_result->item;
|
||
return Koha::Item->_new_from_dbic($item_rs);
|
||
}
|
||
|
||
=head3 account_lines
|
||
|
||
my $account_lines = $checkout->account_lines;
|
||
|
||
Return the checked out account_lines
|
||
|
||
=cut
|
||
|
||
sub account_lines {
|
||
my ($self) = @_;
|
||
my $account_lines_rs = $self->_result->account_lines;
|
||
return Koha::Account::Lines->_new_from_dbic($account_lines_rs);
|
||
}
|
||
|
||
=head3 overdue_fines
|
||
|
||
my $fines = $checkout->overdue_fines;
|
||
|
||
Return the account lines for just the overdue fines
|
||
|
||
=cut
|
||
|
||
sub overdue_fines {
|
||
my ($self) = @_;
|
||
my $account_lines_rs = $self->_result->account_lines->search( { debit_type_code => 'OVERDUE' } );
|
||
return Koha::Account::Lines->_new_from_dbic($account_lines_rs);
|
||
}
|
||
|
||
=head3 library
|
||
|
||
my $library = $checkout->library;
|
||
|
||
Return the library in which the transaction took place
|
||
|
||
=cut
|
||
|
||
sub library {
|
||
my ($self) = @_;
|
||
my $library_rs = $self->_result->library;
|
||
return Koha::Library->_new_from_dbic($library_rs);
|
||
}
|
||
|
||
=head3 patron
|
||
|
||
my $patron = $checkout->patron
|
||
|
||
Return the patron for who the checkout has been done
|
||
|
||
=cut
|
||
|
||
sub patron {
|
||
my ($self) = @_;
|
||
my $patron_rs = $self->_result->patron;
|
||
return Koha::Patron->_new_from_dbic($patron_rs);
|
||
}
|
||
|
||
=head3 issuer
|
||
|
||
my $issuer = $checkout->issuer
|
||
|
||
Return the patron by whom the checkout was done
|
||
|
||
=cut
|
||
|
||
sub issuer {
|
||
my ($self) = @_;
|
||
my $issuer_rs = $self->_result->issuer;
|
||
return unless $issuer_rs;
|
||
return Koha::Patron->_new_from_dbic($issuer_rs);
|
||
}
|
||
|
||
=head3 renewals
|
||
|
||
my $renewals = $checkout->renewals;
|
||
|
||
Return a Koha::Checkouts::Renewals set attached to this checkout
|
||
|
||
=cut
|
||
|
||
sub renewals {
|
||
my ($self) = @_;
|
||
my $renewals_rs = $self->_result->renewals;
|
||
return unless $renewals_rs;
|
||
return Koha::Checkouts::Renewals->_new_from_dbic($renewals_rs);
|
||
}
|
||
|
||
=head3 booking
|
||
|
||
my $booking = $checkout->booking;
|
||
|
||
Return the linked booking
|
||
|
||
=cut
|
||
|
||
sub booking {
|
||
my ($self) = @_;
|
||
my $booking_rs = $self->_result->booking;
|
||
return unless $booking_rs;
|
||
return Koha::Booking->_new_from_dbic($booking_rs);
|
||
}
|
||
|
||
=head3 attempt_auto_renew
|
||
|
||
my ($success, $error, $updated) = $checkout->auto_renew({ confirm => 1 });
|
||
|
||
Attempt to automatically renew a book. Return error reason if it cannot be renewed.
|
||
Also return whether a change has been made to avoid notifying on more than one attempt.
|
||
|
||
If not passed confirm, we will only report and no changes will be made.
|
||
|
||
=cut
|
||
|
||
sub attempt_auto_renew {
|
||
my ( $self, $params ) = @_;
|
||
my $confirm = $params->{confirm} // 0;
|
||
|
||
# CanBookBeRenewed returns 'auto_renew' when the renewal should be done by this script
|
||
my ( $ok, $error ) = C4::Circulation::CanBookBeRenewed( $self->patron, $self, undef, 1 );
|
||
my $store_error;
|
||
if ( $error eq 'auto_renew' || $error eq 'auto_renew_final' || $error eq 'auto_unseen_final' ) {
|
||
if ($confirm) {
|
||
my $date_due = C4::Circulation::AddRenewal(
|
||
{
|
||
borrowernumber => $self->borrowernumber,
|
||
itemnumber => $self->itemnumber,
|
||
branch => $self->branchcode,
|
||
seen => 0,
|
||
automatic => 1,
|
||
}
|
||
);
|
||
$store_error = $error eq 'auto_renew' ? undef : $error;
|
||
$self->auto_renew_error($store_error)->store;
|
||
}
|
||
return ( 1, $store_error, 1 );
|
||
} else {
|
||
my $updated = 0;
|
||
if ( !$self->auto_renew_error || $error ne $self->auto_renew_error ) {
|
||
$updated = 1
|
||
unless (
|
||
$self->auto_renew_error
|
||
&& ( $self->auto_renew_error eq 'auto_renew_final' && $error eq 'too_many'
|
||
|| $self->auto_renew_error eq 'auto_unseen_final' && $error eq 'too_unseen' )
|
||
);
|
||
$self->auto_renew_error($error)->store if $confirm;
|
||
}
|
||
return ( 0, $error, $updated );
|
||
}
|
||
|
||
}
|
||
|
||
=head3 to_api_mapping
|
||
|
||
This method returns the mapping for representing a Koha::Checkout object
|
||
on the API.
|
||
|
||
=cut
|
||
|
||
sub to_api_mapping {
|
||
return {
|
||
checkin_library => 'checkin_library_id',
|
||
issue_id => 'checkout_id',
|
||
borrowernumber => 'patron_id',
|
||
itemnumber => 'item_id',
|
||
booking_id => 'booking_id',
|
||
date_due => 'due_date',
|
||
branchcode => 'library_id',
|
||
returndate => 'checkin_date',
|
||
lastreneweddate => 'last_renewed_date',
|
||
issuedate => 'checkout_date',
|
||
notedate => 'note_date',
|
||
noteseen => 'note_seen',
|
||
};
|
||
}
|
||
|
||
=head3 public_read_list
|
||
|
||
This method returns the list of publicly readable database fields for both API and UI output purposes
|
||
|
||
=cut
|
||
|
||
sub public_read_list {
|
||
return [
|
||
'checkin_library', 'issue_id', 'borrowernumber',
|
||
'itemnumber', 'date_due', 'branchcode',
|
||
'returndate', 'lastreneweddate', 'issuedate',
|
||
'notedate', 'noteseen'
|
||
];
|
||
}
|
||
|
||
=head3 claim_returned
|
||
|
||
my $return_claim = $checkout->claim_returned();
|
||
|
||
This method sets the checkout as claimed return. It will:
|
||
|
||
1. Add a new row to the `return_claims` table
|
||
2. Set the item as lost using the 'ClaimReturnedLostValue'
|
||
3. Charge a fee depending on the value of ClaimReturnedChargeFee
|
||
3a. If set to charge, then accruing overdues will be halted
|
||
3b. If set to charge, then any existing transfers will be cancelled
|
||
and the holding branch will be set back to 'frombranch'.
|
||
4. The issue will be marked as returned as per the 'MarkLostItemsAsReturned' preference
|
||
|
||
=cut
|
||
|
||
sub claim_returned {
|
||
my ( $self, $params ) = @_;
|
||
|
||
my $charge_lost_fee = $params->{charge_lost_fee};
|
||
my $refund_lost_fee = $params->{refund_lost_fee};
|
||
|
||
try {
|
||
$self->_result->result_source->schema->txn_do(
|
||
sub {
|
||
my $claim = Koha::Checkouts::ReturnClaim->new(
|
||
{
|
||
issue_id => $self->id,
|
||
itemnumber => $self->itemnumber,
|
||
borrowernumber => $self->borrowernumber,
|
||
notes => $params->{notes},
|
||
created_by => $params->{created_by},
|
||
created_on => dt_from_string,
|
||
}
|
||
)->store();
|
||
|
||
my $ClaimReturnedLostValue = C4::Context->preference('ClaimReturnedLostValue');
|
||
$self->item->itemlost($ClaimReturnedLostValue)->store unless $self->item->itemlost;
|
||
|
||
my $ClaimReturnedChargeFee = C4::Context->preference('ClaimReturnedChargeFee');
|
||
$charge_lost_fee =
|
||
$ClaimReturnedChargeFee eq 'charge' ? 1
|
||
: $ClaimReturnedChargeFee eq 'no_charge' ? 0
|
||
: $charge_lost_fee; # $ClaimReturnedChargeFee eq 'ask'
|
||
|
||
if ($charge_lost_fee) {
|
||
C4::Circulation::LostItem( $self->itemnumber, 'claim_returned' );
|
||
} elsif ( C4::Context->preference('MarkLostItemsAsReturned') =~ m/claim_returned/ ) {
|
||
C4::Circulation::MarkIssueReturned(
|
||
$self->borrowernumber, $self->itemnumber, undef,
|
||
$self->patron->privacy
|
||
);
|
||
}
|
||
|
||
if ($refund_lost_fee) {
|
||
$self->item->store( { refund_lost_fee => $refund_lost_fee } );
|
||
}
|
||
|
||
return $claim;
|
||
}
|
||
);
|
||
} catch {
|
||
if ( $_->isa('Koha::Exception') ) {
|
||
$_->rethrow();
|
||
} else {
|
||
|
||
# ?
|
||
Koha::Exception->throw("Unhandled exception");
|
||
}
|
||
};
|
||
}
|
||
|
||
=head2 branch_for_fee_context
|
||
|
||
my $branchcode = Koha::Checkout->branch_for_fee_context(
|
||
fee_type => 'OVERDUE' | 'LOST' | 'PROCESSING',
|
||
);
|
||
|
||
Determines which branch’s rules should apply to a fee based on system preferences and available context. For LOST/PROCESSING it honors LostChargesControl; for OVERDUE it honors CircControl. It also respects HomeOrHoldingBranch when choosing the item’s branch. Returns a branchcode (string) or undef if it cannot be determined.
|
||
|
||
=cut
|
||
|
||
sub branch_for_fee_context {
|
||
my ( $class, %args ) = @_;
|
||
|
||
my $fee_type = $args{fee_type} // 'OVERDUE';
|
||
|
||
# Choose the controlling preference depending on fee type
|
||
my $control_pref =
|
||
( $fee_type eq 'LOST' || $fee_type eq 'PROCESSING' )
|
||
? C4::Context->preference('LostChargesControl')
|
||
: C4::Context->preference('CircControl');
|
||
|
||
# Item branch selection (home vs holding)
|
||
my $hoh_pref = C4::Context->preference('HomeOrHoldingBranch') // 'home';
|
||
my $use_holding = ( $hoh_pref =~ /holding/i ) ? 1 : 0;
|
||
|
||
# Support Koha objects OR plain hashes
|
||
my $patron_branch = eval { $args{patron}->branchcode } // ( $args{patron} && $args{patron}->{branchcode} );
|
||
my $issuing_branch = eval { $args{issue}->branchcode } // ( $args{issue} && $args{issue}->{branchcode} );
|
||
my $item_home = eval { $args{item}->homebranch } // ( $args{item} && $args{item}->{homebranch} );
|
||
my $item_holding = eval { $args{item}->holdingbranch } // ( $args{item} && $args{item}->{holdingbranch} );
|
||
my $item_branch = $use_holding ? ( $item_holding // $item_home ) : $item_home;
|
||
|
||
# Normalize empty strings to undef so // works as intended
|
||
for my $ref ( \$patron_branch, \$issuing_branch, \$item_home, \$item_holding, \$item_branch ) {
|
||
$$ref = undef if defined $$ref && $$ref eq '';
|
||
}
|
||
|
||
# Resolution order depends on controlling pref
|
||
my $resolved =
|
||
( defined $control_pref && $control_pref eq 'PatronLibrary' ) ? $patron_branch
|
||
: ( defined $control_pref && $control_pref eq 'PickupLibrary' )
|
||
? ( $issuing_branch // $patron_branch // $item_branch )
|
||
: $item_branch; # ItemLibrary (default)
|
||
return $resolved;
|
||
}
|
||
|
||
=head2 Internal methods
|
||
|
||
=head3 _type
|
||
|
||
=cut
|
||
|
||
sub _type {
|
||
return 'Issue';
|
||
}
|
||
|
||
=head1 AUTHOR
|
||
|
||
Kyle M Hall <kyle@bywatersolutions.com>
|
||
|
||
Jonathan Druart <jonathan.druart@bugs.koha-community.org>
|
||
|
||
=cut
|
||
|
||
1;
|