Main Koha release repository https://koha-community.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

4411 lines
158 KiB

package C4::Circulation;
# Copyright 2000-2002 Katipo Communications
# copyright 2010 BibLibre
#
# 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 DateTime;
use POSIX qw( floor );
use Koha::DateUtils;
use C4::Context;
use C4::Stats;
use C4::Reserves;
use C4::Biblio;
use C4::Items;
use C4::Members;
use C4::Accounts;
use C4::ItemCirculationAlertPreference;
use C4::Message;
use C4::Debug;
use C4::Log; # logaction
use C4::Overdues qw(CalcFine UpdateFine get_chargeable_units);
use C4::RotatingCollections qw(GetCollectionItemBranches);
use Algorithm::CheckDigits;
use Data::Dumper;
use Koha::Account;
use Koha::AuthorisedValues;
use Koha::Biblioitems;
use Koha::DateUtils;
use Koha::Calendar;
use Koha::Checkouts;
use Koha::Illrequests;
use Koha::Items;
use Koha::Patrons;
use Koha::Patron::Debarments;
use Koha::Database;
use Koha::Libraries;
use Koha::Account::Lines;
use Koha::Holds;
use Koha::Account::Lines;
use Koha::Account::Offsets;
use Koha::Config::SysPrefs;
use Koha::Charges::Fees;
use Koha::Util::SystemPreferences;
use Koha::Checkouts::ReturnClaims;
use Koha::SearchEngine::Indexer;
use Carp;
use List::MoreUtils qw( uniq any );
use Scalar::Util qw( looks_like_number );
use Try::Tiny;
use Date::Calc qw(
Today
Today_and_Now
Add_Delta_YM
Add_Delta_DHMS
Date_to_Days
Day_of_Week
Add_Delta_Days
);
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
BEGIN {
require Exporter;
@ISA = qw(Exporter);
# FIXME subs that should probably be elsewhere
push @EXPORT, qw(
&barcodedecode
&LostItem
&ReturnLostItem
&GetPendingOnSiteCheckouts
);
# subs to deal with issuing a book
push @EXPORT, qw(
&CanBookBeIssued
&CanBookBeRenewed
&AddIssue
&AddRenewal
&GetRenewCount
&GetSoonestRenewDate
&GetLatestAutoRenewDate
&GetIssuingCharges
&GetBranchBorrowerCircRule
&GetBranchItemRule
&GetBiblioIssues
&GetOpenIssue
&CheckIfIssuedToPatron
&IsItemIssued
GetTopIssues
);
# subs to deal with returns
push @EXPORT, qw(
&AddReturn
&MarkIssueReturned
);
# subs to deal with transfers
push @EXPORT, qw(
&transferbook
&GetTransfers
&GetTransfersFromTo
&updateWrongTransfer
&DeleteTransfer
&IsBranchTransferAllowed
&CreateBranchTransferLimit
&DeleteBranchTransferLimits
&TransferSlip
);
# subs to deal with offline circulation
push @EXPORT, qw(
&GetOfflineOperations
&GetOfflineOperation
&AddOfflineOperation
&DeleteOfflineOperation
&ProcessOfflineOperation
);
}
=head1 NAME
C4::Circulation - Koha circulation module
=head1 SYNOPSIS
use C4::Circulation;
=head1 DESCRIPTION
The functions in this module deal with circulation, issues, and
returns, as well as general information about the library.
Also deals with inventory.
=head1 FUNCTIONS
=head2 barcodedecode
$str = &barcodedecode($barcode, [$filter]);
Generic filter function for barcode string.
Called on every circ if the System Pref itemBarcodeInputFilter is set.
Will do some manipulation of the barcode for systems that deliver a barcode
to circulation.pl that differs from the barcode stored for the item.
For proper functioning of this filter, calling the function on the
correct barcode string (items.barcode) should return an unaltered barcode.
The optional $filter argument is to allow for testing or explicit
behavior that ignores the System Pref. Valid values are the same as the
System Pref options.
=cut
# FIXME -- the &decode fcn below should be wrapped into this one.
# FIXME -- these plugins should be moved out of Circulation.pm
#
sub barcodedecode {
my ($barcode, $filter) = @_;
my $branch = C4::Context::mybranch();
$filter = C4::Context->preference('itemBarcodeInputFilter') unless $filter;
$filter or return $barcode; # ensure filter is defined, else return untouched barcode
if ($filter eq 'whitespace') {
$barcode =~ s/\s//g;
} elsif ($filter eq 'cuecat') {
chomp($barcode);
my @fields = split( /\./, $barcode );
my @results = map( decode($_), @fields[ 1 .. $#fields ] );
($#results == 2) and return $results[2];
} elsif ($filter eq 'T-prefix') {
if ($barcode =~ /^[Tt](\d)/) {
(defined($1) and $1 eq '0') and return $barcode;
$barcode = substr($barcode, 2) + 0; # FIXME: probably should be substr($barcode, 1)
}
return sprintf("T%07d", $barcode);
# FIXME: $barcode could be "T1", causing warning: substr outside of string
# Why drop the nonzero digit after the T?
# Why pass non-digits (or empty string) to "T%07d"?
} elsif ($filter eq 'libsuite8') {
unless($barcode =~ m/^($branch)-/i){ #if barcode starts with branch code its in Koha style. Skip it.
if($barcode =~ m/^(\d)/i){ #Some barcodes even start with 0's & numbers and are assumed to have b as the item type in the libsuite8 software
$barcode =~ s/^[0]*(\d+)$/$branch-b-$1/i;
}else{
$barcode =~ s/^(\D+)[0]*(\d+)$/$branch-$1-$2/i;
}
}
} elsif ($filter eq 'EAN13') {
my $ean = CheckDigits('ean');
if ( $ean->is_valid($barcode) ) {
#$barcode = sprintf('%013d',$barcode); # this doesn't work on 32-bit systems
$barcode = '0' x ( 13 - length($barcode) ) . $barcode;
} else {
warn "# [$barcode] not valid EAN-13/UPC-A\n";
}
}
return $barcode; # return barcode, modified or not
}
=head2 decode
$str = &decode($chunk);
Decodes a segment of a string emitted by a CueCat barcode scanner and
returns it.
FIXME: Should be replaced with Barcode::Cuecat from CPAN
or Javascript based decoding on the client side.
=cut
sub decode {
my ($encoded) = @_;
my $seq =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-';
my @s = map { index( $seq, $_ ); } split( //, $encoded );
my $l = ( $#s + 1 ) % 4;
if ($l) {
if ( $l == 1 ) {
# warn "Error: Cuecat decode parsing failed!";
return;
}
$l = 4 - $l;
$#s += $l;
}
my $r = '';
while ( $#s >= 0 ) {
my $n = ( ( $s[0] << 6 | $s[1] ) << 6 | $s[2] ) << 6 | $s[3];
$r .=
chr( ( $n >> 16 ) ^ 67 )
.chr( ( $n >> 8 & 255 ) ^ 67 )
.chr( ( $n & 255 ) ^ 67 );
@s = @s[ 4 .. $#s ];
}
$r = substr( $r, 0, length($r) - $l );
return $r;
}
=head2 transferbook
($dotransfer, $messages, $iteminformation) = &transferbook({
from_branch => $frombranch
to_branch => $tobranch,
barcode => $barcode,
ignore_reserves => $ignore_reserves,
trigger => $trigger
});
Transfers an item to a new branch. If the item is currently on loan, it is automatically returned before the actual transfer.
C<$fbr> is the code for the branch initiating the transfer.
C<$tbr> is the code for the branch to which the item should be transferred.
C<$barcode> is the barcode of the item to be transferred.
If C<$ignore_reserves> is true, C<&transferbook> ignores reserves.
Otherwise, if an item is reserved, the transfer fails.
C<$trigger> is the enum value for what triggered the transfer.
Returns three values:
=over
=item $dotransfer
is true if the transfer was successful.
=item $messages
is a reference-to-hash which may have any of the following keys:
=over
=item C<BadBarcode>
There is no item in the catalog with the given barcode. The value is C<$barcode>.
=item C<DestinationEqualsHolding>
The item is already at the branch to which it is being transferred. The transfer is nonetheless considered to have failed. The value should be ignored.
=item C<WasReturned>
The item was on loan, and C<&transferbook> automatically returned it before transferring it. The value is the borrower number of the patron who had the item.
=item C<ResFound>
The item was reserved. The value is a reference-to-hash whose keys are fields from the reserves table of the Koha database, and C<biblioitemnumber>. It also has the key C<ResFound>, whose value is either C<Waiting> or C<Reserved>.
=item C<WasTransferred>
The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored.
=back
=back
=cut
sub transferbook {
my $params = shift;
my $tbr = $params->{to_branch};
my $fbr = $params->{from_branch};
my $ignoreRs = $params->{ignore_reserves};
my $barcode = $params->{barcode};
my $trigger = $params->{trigger};
my $messages;
my $dotransfer = 1;
my $item = Koha::Items->find( { barcode => $barcode } );
Koha::Exceptions::MissingParameter->throw(
"Missing mandatory parameter: from_branch")
unless $fbr;
Koha::Exceptions::MissingParameter->throw(
"Missing mandatory parameter: to_branch")
unless $tbr;
# bad barcode..
unless ( $item ) {
$messages->{'BadBarcode'} = $barcode;
$dotransfer = 0;
return ( $dotransfer, $messages );
}
my $itemnumber = $item->itemnumber;
# get branches of book...
my $hbr = $item->homebranch;
# if using Branch Transfer Limits
if ( C4::Context->preference("UseBranchTransferLimits") == 1 ) {
my $code = C4::Context->preference("BranchTransferLimitsType") eq 'ccode' ? $item->ccode : $item->biblio->biblioitem->itemtype; # BranchTransferLimitsType is 'ccode' or 'itemtype'
if ( C4::Context->preference("item-level_itypes") && C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ) {
if ( ! IsBranchTransferAllowed( $tbr, $fbr, $item->itype ) ) {
$messages->{'NotAllowed'} = $tbr . "::" . $item->itype;
$dotransfer = 0;
}
} elsif ( ! IsBranchTransferAllowed( $tbr, $fbr, $code ) ) {
$messages->{'NotAllowed'} = $tbr . "::" . $code;
$dotransfer = 0;
}
}
# can't transfer book if is already there....
if ( $fbr eq $tbr ) {
$messages->{'DestinationEqualsHolding'} = 1;
$dotransfer = 0;
}
# check if it is still issued to someone, return it...
my $issue = Koha::Checkouts->find({ itemnumber => $itemnumber });
if ( $issue ) {
AddReturn( $barcode, $fbr );
$messages->{'WasReturned'} = $issue->borrowernumber;
}
# find reserves.....
# That'll save a database query.
my ( $resfound, $resrec, undef ) =
CheckReserves( $itemnumber );
if ( $resfound and not $ignoreRs ) {
$resrec->{'ResFound'} = $resfound;
$messages->{'ResFound'} = $resrec;
$dotransfer = 1;
}
#actually do the transfer....
if ($dotransfer) {
ModItemTransfer( $itemnumber, $fbr, $tbr, $trigger );
# don't need to update MARC anymore, we do it in batch now
$messages->{'WasTransfered'} = 1;
}
ModDateLastSeen( $itemnumber );
return ( $dotransfer, $messages );
}
sub TooMany {
my $borrower = shift;
my $item_object = shift;
my $params = shift;
my $onsite_checkout = $params->{onsite_checkout} || 0;
my $switch_onsite_checkout = $params->{switch_onsite_checkout} || 0;
my $cat_borrower = $borrower->{'categorycode'};
my $dbh = C4::Context->dbh;
# Get which branchcode we need
my $branch = _GetCircControlBranch($item_object->unblessed,$borrower);
my $type = $item_object->effective_itemtype;
my ($type_object, $parent_type, $parent_maxissueqty_rule);
$type_object = Koha::ItemTypes->find( $type );
$parent_type = $type_object->parent_type if $type_object;
my $child_types = Koha::ItemTypes->search({ parent_type => $type });
# Find any children if we are a parent_type;
# given branch, patron category, and item type, determine
# applicable issuing rule
$parent_maxissueqty_rule = Koha::CirculationRules->get_effective_rule(
{
categorycode => $cat_borrower,
itemtype => $parent_type,
branchcode => $branch,
rule_name => 'maxissueqty',
}
) if $parent_type;
# If the parent rule is for default type we discount it
$parent_maxissueqty_rule = undef if $parent_maxissueqty_rule && !defined $parent_maxissueqty_rule->itemtype;
my $maxissueqty_rule = Koha::CirculationRules->get_effective_rule(
{
categorycode => $cat_borrower,
itemtype => $type,
branchcode => $branch,
rule_name => 'maxissueqty',
}
);
my $maxonsiteissueqty_rule = Koha::CirculationRules->get_effective_rule(
{
categorycode => $cat_borrower,
itemtype => $type,
branchcode => $branch,
rule_name => 'maxonsiteissueqty',
}
);
my $patron = Koha::Patrons->find($borrower->{borrowernumber});
# if a rule is found and has a loan limit set, count
# how many loans the patron already has that meet that
# rule
if (defined($maxissueqty_rule) and $maxissueqty_rule->rule_value ne "") {
my $checkouts;
if ( $maxissueqty_rule->branchcode ) {
if ( C4::Context->preference('CircControl') eq 'PickupLibrary' ) {
$checkouts = $patron->checkouts->search(
{ 'me.branchcode' => $maxissueqty_rule->branchcode } );
} elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
$checkouts = $patron->checkouts; # if branch is the patron's home branch, then count all loans by patron
} else {
$checkouts = $patron->checkouts->search(
{ 'item.homebranch' => $maxissueqty_rule->branchcode },
{ prefetch => 'item' } );
}
} else {
$checkouts = $patron->checkouts; # if rule is not branch specific then count all loans by patron
}
my $sum_checkouts;
my $rule_itemtype = $maxissueqty_rule->itemtype;
while ( my $c = $checkouts->next ) {
my $itemtype = $c->item->effective_itemtype;
my @types;
unless ( $rule_itemtype ) {
# matching rule has the default item type, so count only
# those existing loans that don't fall under a more
# specific rule
@types = Koha::CirculationRules->search(
{
branchcode => $maxissueqty_rule->branchcode,
categorycode => [ $maxissueqty_rule->categorycode, $cat_borrower ],
itemtype => { '!=' => undef },
rule_name => 'maxissueqty'
}
)->get_column('itemtype');
next if grep {$_ eq $itemtype} @types;
} else {
my @types;
if ( $parent_maxissueqty_rule ) {
# if we have a parent item type then we count loans of the
# specific item type or its siblings or parent
my $children = Koha::ItemTypes->search({ parent_type => $parent_type });
@types = $children->get_column('itemtype');
push @types, $parent_type;
} elsif ( $child_types ) {
# If we are a parent type, we need to count all child types and our own type
@types = $child_types->get_column('itemtype');
push @types, $type; # And don't forget to count our own types
} else { push @types, $type; } # Otherwise only count the specific itemtype
next unless grep {$_ eq $itemtype} @types;
}
$sum_checkouts->{total}++;
$sum_checkouts->{onsite_checkouts}++ if $c->onsite_checkout;
$sum_checkouts->{itemtype}->{$itemtype}++;
}
my $checkout_count_type = $sum_checkouts->{itemtype}->{$type} || 0;
my $checkout_count = $sum_checkouts->{total} || 0;
my $onsite_checkout_count = $sum_checkouts->{onsite_checkouts} || 0;
my $checkout_rules = {
checkout_count => $checkout_count,
onsite_checkout_count => $onsite_checkout_count,
onsite_checkout => $onsite_checkout,
max_checkouts_allowed => $maxissueqty_rule ? $maxissueqty_rule->rule_value : undef,
max_onsite_checkouts_allowed => $maxonsiteissueqty_rule ? $maxonsiteissueqty_rule->rule_value : undef,
switch_onsite_checkout => $switch_onsite_checkout,
};
# If parent rules exists
if ( defined($parent_maxissueqty_rule) and defined($parent_maxissueqty_rule->rule_value) ){
$checkout_rules->{max_checkouts_allowed} = $parent_maxissueqty_rule ? $parent_maxissueqty_rule->rule_value : undef;
my $qty_over = _check_max_qty($checkout_rules);
return $qty_over if defined $qty_over;
# If the parent rule is less than or equal to the child, we only need check the parent
if( $maxissueqty_rule->rule_value < $parent_maxissueqty_rule->rule_value && defined($maxissueqty_rule->itemtype) ) {
$checkout_rules->{checkout_count} = $checkout_count_type;
$checkout_rules->{max_checkouts_allowed} = $maxissueqty_rule ? $maxissueqty_rule->rule_value : undef;
my $qty_over = _check_max_qty($checkout_rules);
return $qty_over if defined $qty_over;
}
} else {
my $qty_over = _check_max_qty($checkout_rules);
return $qty_over if defined $qty_over;
}
}
# Now count total loans against the limit for the branch
my $branch_borrower_circ_rule = GetBranchBorrowerCircRule($branch, $cat_borrower);
if (defined($branch_borrower_circ_rule->{patron_maxissueqty}) and $branch_borrower_circ_rule->{patron_maxissueqty} ne '') {
my $checkouts;
if ( C4::Context->preference('CircControl') eq 'PickupLibrary' ) {
$checkouts = $patron->checkouts->search(
{ 'me.branchcode' => $branch} );
} elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
$checkouts = $patron->checkouts; # if branch is the patron's home branch, then count all loans by patron
} else {
$checkouts = $patron->checkouts->search(
{ 'item.homebranch' => $branch},
{ prefetch => 'item' } );
}
my $checkout_count = $checkouts->count;
my $onsite_checkout_count = $checkouts->search({ onsite_checkout => 1 })->count;
my $max_checkouts_allowed = $branch_borrower_circ_rule->{patron_maxissueqty};
my $max_onsite_checkouts_allowed = $branch_borrower_circ_rule->{patron_maxonsiteissueqty} || undef;
my $qty_over = _check_max_qty(
{
checkout_count => $checkout_count,
onsite_checkout_count => $onsite_checkout_count,
onsite_checkout => $onsite_checkout,
max_checkouts_allowed => $max_checkouts_allowed,
max_onsite_checkouts_allowed => $max_onsite_checkouts_allowed,
switch_onsite_checkout => $switch_onsite_checkout
}
);
return $qty_over if defined $qty_over;
}
if ( not defined( $maxissueqty_rule ) and not defined($branch_borrower_circ_rule->{patron_maxissueqty}) ) {
return { reason => 'NO_RULE_DEFINED', max_allowed => 0 };
}
# OK, the patron can issue !!!
return;
}
sub _check_max_qty {
my $params = shift;
my $checkout_count = $params->{checkout_count};
my $onsite_checkout_count = $params->{onsite_checkout_count};
my $onsite_checkout = $params->{onsite_checkout};
my $max_checkouts_allowed = $params->{max_checkouts_allowed};
my $max_onsite_checkouts_allowed = $params->{max_onsite_checkouts_allowed};
my $switch_onsite_checkout = $params->{switch_onsite_checkout};
if ( $onsite_checkout and defined $max_onsite_checkouts_allowed ) {
if ( $max_onsite_checkouts_allowed eq '' ) { return; }
if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed ) {
return {
reason => 'TOO_MANY_ONSITE_CHECKOUTS',
count => $onsite_checkout_count,
max_allowed => $max_onsite_checkouts_allowed,
};
}
}
if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) {
if ( $max_checkouts_allowed eq '' ) { return; }
my $delta = $switch_onsite_checkout ? 1 : 0;
if ( $checkout_count >= $max_checkouts_allowed + $delta ) {
return {
reason => 'TOO_MANY_CHECKOUTS',
count => $checkout_count,
max_allowed => $max_checkouts_allowed,
};
}
}
elsif ( not $onsite_checkout ) {
if ( $max_checkouts_allowed eq '' ) { return; }
if (
$checkout_count - $onsite_checkout_count >= $max_checkouts_allowed )
{
return {
reason => 'TOO_MANY_CHECKOUTS',
count => $checkout_count - $onsite_checkout_count,
max_allowed => $max_checkouts_allowed,
};
}
}
return;
}
=head2 CanBookBeIssued
( $issuingimpossible, $needsconfirmation, [ $alerts ] ) = CanBookBeIssued( $patron,
$barcode, $duedate, $inprocess, $ignore_reserves, $params );
Check if a book can be issued.
C<$issuingimpossible> and C<$needsconfirmation> are hashrefs.
IMPORTANT: The assumption by users of this routine is that causes blocking
the issue are keyed by uppercase labels and other returned
data is keyed in lower case!
=over 4
=item C<$patron> is a Koha::Patron
=item C<$barcode> is the bar code of the book being issued.
=item C<$duedates> is a DateTime object.
=item C<$inprocess> boolean switch
=item C<$ignore_reserves> boolean switch
=item C<$params> Hashref of additional parameters
Available keys:
override_high_holds - Ignore high holds
onsite_checkout - Checkout is an onsite checkout that will not leave the library
=back
Returns :
=over 4
=item C<$issuingimpossible> a reference to a hash. It contains reasons why issuing is impossible.
Possible values are :
=back
=head3 INVALID_DATE
sticky due date is invalid
=head3 GNA
borrower gone with no address
=head3 CARD_LOST
borrower declared it's card lost
=head3 DEBARRED
borrower debarred
=head3 UNKNOWN_BARCODE
barcode unknown
=head3 NOT_FOR_LOAN
item is not for loan
=head3 WTHDRAWN
item withdrawn.
=head3 RESTRICTED
item is restricted (set by ??)
C<$needsconfirmation> a reference to a hash. It contains reasons why the loan
could be prevented, but ones that can be overriden by the operator.
Possible values are :
=head3 DEBT
borrower has debts.
=head3 RENEW_ISSUE
renewing, not issuing
=head3 ISSUED_TO_ANOTHER
issued to someone else.
=head3 RESERVED
reserved for someone else.
=head3 INVALID_DATE
sticky due date is invalid or due date in the past
=head3 TOO_MANY
if the borrower borrows to much things
=cut
sub CanBookBeIssued {
my ( $patron, $barcode, $duedate, $inprocess, $ignore_reserves, $params ) = @_;
my %needsconfirmation; # filled with problems that needs confirmations
my %issuingimpossible; # filled with problems that causes the issue to be IMPOSSIBLE
my %alerts; # filled with messages that shouldn't stop issuing, but the librarian should be aware of.
my %messages; # filled with information messages that should be displayed.
my $onsite_checkout = $params->{onsite_checkout} || 0;
my $override_high_holds = $params->{override_high_holds} || 0;
my $item_object = Koha::Items->find({barcode => $barcode });
# MANDATORY CHECKS - unless item exists, nothing else matters
unless ( $item_object ) {
$issuingimpossible{UNKNOWN_BARCODE} = 1;
}
return ( \%issuingimpossible, \%needsconfirmation ) if %issuingimpossible;
my $item_unblessed = $item_object->unblessed; # Transition...
my $issue = $item_object->checkout;
my $biblio = $item_object->biblio;
my $biblioitem = $biblio->biblioitem;
my $effective_itemtype = $item_object->effective_itemtype;
my $dbh = C4::Context->dbh;
my $patron_unblessed = $patron->unblessed;
my $circ_library = Koha::Libraries->find( _GetCircControlBranch($item_unblessed, $patron_unblessed) );
#
# DUE DATE is OK ? -- should already have checked.
#
if ($duedate && ref $duedate ne 'DateTime') {
$duedate = dt_from_string($duedate);
}
my $now = dt_from_string();
unless ( $duedate ) {
my $issuedate = $now->clone();
$duedate = CalcDateDue( $issuedate, $effective_itemtype, $circ_library->branchcode, $patron_unblessed );
# Offline circ calls AddIssue directly, doesn't run through here
# So issuingimpossible should be ok.
}
my $fees = Koha::Charges::Fees->new(
{
patron => $patron,
library => $circ_library,
item => $item_object,
to_date => $duedate,
}
);
if ($duedate) {
my $today = $now->clone();
$today->truncate( to => 'minute');
if (DateTime->compare($duedate,$today) == -1 ) { # duedate cannot be before now
$needsconfirmation{INVALID_DATE} = output_pref($duedate);
}
} else {
$issuingimpossible{INVALID_DATE} = output_pref($duedate);
}
#
# BORROWER STATUS
#
if ( $patron->category->category_type eq 'X' && ( $item_object->barcode )) {
# stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1 .
&UpdateStats({
branch => C4::Context->userenv->{'branch'},
type => 'localuse',
itemnumber => $item_object->itemnumber,
itemtype => $effective_itemtype,
borrowernumber => $patron->borrowernumber,
ccode => $item_object->ccode}
);
ModDateLastSeen( $item_object->itemnumber ); # FIXME Move to Koha::Item
return( { STATS => 1 }, {});
}
if ( $patron->gonenoaddress && $patron->gonenoaddress == 1 ) {
$issuingimpossible{GNA} = 1;
}
if ( $patron->lost && $patron->lost == 1 ) {
$issuingimpossible{CARD_LOST} = 1;
}
if ( $patron->is_debarred ) {
$issuingimpossible{DEBARRED} = 1;
}
if ( $patron->is_expired ) {
$issuingimpossible{EXPIRED} = 1;
}
#
# BORROWER STATUS
#
# DEBTS
my $account = $patron->account;
my $balance = $account->balance;
my $non_issues_charges = $account->non_issues_charges;
my $other_charges = $balance - $non_issues_charges;
my $amountlimit = C4::Context->preference("noissuescharge");
my $allowfineoverride = C4::Context->preference("AllowFineOverride");
my $allfinesneedoverride = C4::Context->preference("AllFinesNeedOverride");
# Check the debt of this patrons guarantees
my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
$no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
if ( defined $no_issues_charge_guarantees ) {
my @guarantees = map { $_->guarantee } $patron->guarantee_relationships();
my $guarantees_non_issues_charges;
foreach my $g ( @guarantees ) {
$guarantees_non_issues_charges += $g->account->non_issues_charges;
}
if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && !$allowfineoverride) {
$issuingimpossible{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
} elsif ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && $allowfineoverride) {
$needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
} elsif ( $allfinesneedoverride && $guarantees_non_issues_charges > 0 && $guarantees_non_issues_charges <= $no_issues_charge_guarantees && !$inprocess ) {
$needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
}
}
# Check the debt of this patrons guarantors *and* the guarantees of those guarantors
my $no_issues_charge_guarantors = C4::Context->preference("NoIssuesChargeGuarantorsWithGuarantees");
$no_issues_charge_guarantors = undef unless looks_like_number( $no_issues_charge_guarantors );
if ( defined $no_issues_charge_guarantors ) {
my $guarantors_non_issues_charges += $patron->relationships_debt({ include_guarantors => 1, only_this_guarantor => 0, include_this_patron => 1 });
if ( $guarantors_non_issues_charges > $no_issues_charge_guarantors && !$inprocess && !$allowfineoverride) {
$issuingimpossible{DEBT_GUARANTORS} = $guarantors_non_issues_charges;
} elsif ( $guarantors_non_issues_charges > $no_issues_charge_guarantors && !$inprocess && $allowfineoverride) {
$needsconfirmation{DEBT_GUARANTORS} = $guarantors_non_issues_charges;
} elsif ( $allfinesneedoverride && $guarantors_non_issues_charges > 0 && $guarantors_non_issues_charges <= $no_issues_charge_guarantors && !$inprocess ) {
$needsconfirmation{DEBT_GUARANTORS} = $guarantors_non_issues_charges;
}
}
if ( C4::Context->preference("IssuingInProcess") ) {
if ( $non_issues_charges > $amountlimit && !$inprocess && !$allowfineoverride) {
$issuingimpossible{DEBT} = $non_issues_charges;
} elsif ( $non_issues_charges > $amountlimit && !$inprocess && $allowfineoverride) {
$needsconfirmation{DEBT} = $non_issues_charges;
} elsif ( $allfinesneedoverride && $non_issues_charges > 0 && $non_issues_charges <= $amountlimit && !$inprocess ) {
$needsconfirmation{DEBT} = $non_issues_charges;
}
}
else {
if ( $non_issues_charges > $amountlimit && $allowfineoverride ) {
$needsconfirmation{DEBT} = $non_issues_charges;
} elsif ( $non_issues_charges > $amountlimit && !$allowfineoverride) {
$issuingimpossible{DEBT} = $non_issues_charges;
} elsif ( $non_issues_charges > 0 && $allfinesneedoverride ) {
$needsconfirmation{DEBT} = $non_issues_charges;
}
}
if ($balance > 0 && $other_charges > 0) {
$alerts{OTHER_CHARGES} = sprintf( "%.2f", $other_charges );
}
$patron = Koha::Patrons->find( $patron->borrowernumber ); # FIXME Refetch just in case, to avoid regressions. But must not be needed
$patron_unblessed = $patron->unblessed;
if ( my $debarred_date = $patron->is_debarred ) {
# patron has accrued fine days or has a restriction. $count is a date
if ($debarred_date eq '9999-12-31') {
$issuingimpossible{USERBLOCKEDNOENDDATE} = $debarred_date;
}
else {
$issuingimpossible{USERBLOCKEDWITHENDDATE} = $debarred_date;
}
} elsif ( my $num_overdues = $patron->has_overdues ) {
## patron has outstanding overdue loans
if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){
$issuingimpossible{USERBLOCKEDOVERDUE} = $num_overdues;
}
elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){
$needsconfirmation{USERBLOCKEDOVERDUE} = $num_overdues;
}
}
# Additional Materials Check
if ( C4::Context->preference("CircConfirmItemParts")
&& $item_object->materials )
{
$needsconfirmation{ADDITIONAL_MATERIALS} = $item_object->materials;
}
#
# CHECK IF BOOK ALREADY ISSUED TO THIS BORROWER
#
if ( $issue && $issue->borrowernumber eq $patron->borrowernumber ){
# Already issued to current borrower.
# If it is an on-site checkout if it can be switched to a normal checkout
# or ask whether the loan should be renewed
if ( $issue->onsite_checkout
and C4::Context->preference('SwitchOnSiteCheckouts') ) {
$messages{ONSITE_CHECKOUT_WILL_BE_SWITCHED} = 1;
} else {
my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed(
$patron->borrowernumber,
$item_object->itemnumber,
);
if ( $CanBookBeRenewed == 0 ) { # no more renewals allowed
if ( $renewerror eq 'onsite_checkout' ) {
$issuingimpossible{NO_RENEWAL_FOR_ONSITE_CHECKOUTS} = 1;
}
else {
$issuingimpossible{NO_MORE_RENEWALS} = 1;
}
}
else {
$needsconfirmation{RENEW_ISSUE} = 1;
}
}
}
elsif ( $issue ) {
# issued to someone else
my $patron = Koha::Patrons->find( $issue->borrowernumber );
my ( $can_be_returned, $message ) = CanBookBeReturned( $item_unblessed, C4::Context->userenv->{branch} );
unless ( $can_be_returned ) {
$issuingimpossible{RETURN_IMPOSSIBLE} = 1;
$issuingimpossible{branch_to_return} = $message;
} else {
if ( C4::Context->preference('AutoReturnCheckedOutItems') ) {
$alerts{RETURNED_FROM_ANOTHER} = { patron => $patron };
} else {
$needsconfirmation{ISSUED_TO_ANOTHER} = 1;
$needsconfirmation{issued_firstname} = $patron->firstname;
$needsconfirmation{issued_surname} = $patron->surname;
$needsconfirmation{issued_cardnumber} = $patron->cardnumber;
$needsconfirmation{issued_borrowernumber} = $patron->borrowernumber;
}
}
}
# JB34 CHECKS IF BORROWERS DON'T HAVE ISSUE TOO MANY BOOKS
#
my $switch_onsite_checkout = (
C4::Context->preference('SwitchOnSiteCheckouts')
and $issue
and $issue->onsite_checkout
and $issue->borrowernumber == $patron->borrowernumber ? 1 : 0 );
my $toomany = TooMany( $patron_unblessed, $item_object, { onsite_checkout => $onsite_checkout, switch_onsite_checkout => $switch_onsite_checkout, } );
# if TooMany max_allowed returns 0 the user doesn't have permission to check out this book
if ( $toomany && not exists $needsconfirmation{RENEW_ISSUE} ) {
if ( $toomany->{max_allowed} == 0 ) {
$needsconfirmation{PATRON_CANT} = 1;
}
if ( C4::Context->preference("AllowTooManyOverride") ) {
$needsconfirmation{TOO_MANY} = $toomany->{reason};
$needsconfirmation{current_loan_count} = $toomany->{count};
$needsconfirmation{max_loans_allowed} = $toomany->{max_allowed};
} else {
$issuingimpossible{TOO_MANY} = $toomany->{reason};
$issuingimpossible{current_loan_count} = $toomany->{count};
$issuingimpossible{max_loans_allowed} = $toomany->{max_allowed};
}
}
#
# CHECKPREVCHECKOUT: CHECK IF ITEM HAS EVER BEEN LENT TO PATRON
#
$patron = Koha::Patrons->find( $patron->borrowernumber ); # FIXME Refetch just in case, to avoid regressions. But must not be needed
my $wants_check = $patron->wants_check_for_previous_checkout;
$needsconfirmation{PREVISSUE} = 1
if ($wants_check and $patron->do_check_for_previous_checkout($item_unblessed));
#
# ITEM CHECKING
#
if ( $item_object->notforloan )
{
if(!C4::Context->preference("AllowNotForLoanOverride")){
$issuingimpossible{NOT_FOR_LOAN} = 1;
$issuingimpossible{item_notforloan} = $item_object->notforloan;
}else{
$needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
$needsconfirmation{item_notforloan} = $item_object->notforloan;
}
}
else {
# we have to check itemtypes.notforloan also
if (C4::Context->preference('item-level_itypes')){
# this should probably be a subroutine
my $sth = $dbh->prepare("SELECT notforloan FROM itemtypes WHERE itemtype = ?");
$sth->execute($effective_itemtype);
my $notforloan=$sth->fetchrow_hashref();
if ($notforloan->{'notforloan'}) {
if (!C4::Context->preference("AllowNotForLoanOverride")) {
$issuingimpossible{NOT_FOR_LOAN} = 1;
$issuingimpossible{itemtype_notforloan} = $effective_itemtype;
} else {
$needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
$needsconfirmation{itemtype_notforloan} = $effective_itemtype;
}
}
}
else {
my $itemtype = Koha::ItemTypes->find($biblioitem->itemtype);
if ( $itemtype && defined $itemtype->notforloan && $itemtype->notforloan == 1){
if (!C4::Context->preference("AllowNotForLoanOverride")) {
$issuingimpossible{NOT_FOR_LOAN} = 1;
$issuingimpossible{itemtype_notforloan} = $effective_itemtype;
} else {
$needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
$needsconfirmation{itemtype_notforloan} = $effective_itemtype;
}
}
}
}
if ( $item_object->withdrawn && $item_object->withdrawn > 0 )
{
$issuingimpossible{WTHDRAWN} = 1;
}
if ( $item_object->restricted
&& $item_object->restricted == 1 )
{
$issuingimpossible{RESTRICTED} = 1;
}
if ( $item_object->itemlost && C4::Context->preference("IssueLostItem") ne 'nothing' ) {
my $av = Koha::AuthorisedValues->search({ category => 'LOST', authorised_value => $item_object->itemlost });
my $code = $av->count ? $av->next->lib : '';
$needsconfirmation{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'confirm' );
$alerts{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'alert' );
}
if ( C4::Context->preference("IndependentBranches") ) {
my $userenv = C4::Context->userenv;
unless ( C4::Context->IsSuperLibrarian() ) {
my $HomeOrHoldingBranch = C4::Context->preference("HomeOrHoldingBranch");
if ( $item_object->$HomeOrHoldingBranch ne $userenv->{branch} ){
$issuingimpossible{ITEMNOTSAMEBRANCH} = 1;
$issuingimpossible{'itemhomebranch'} = $item_object->$HomeOrHoldingBranch;
}
$needsconfirmation{BORRNOTSAMEBRANCH} = $patron->branchcode
if ( $patron->branchcode ne $userenv->{branch} );
}
}
#
# CHECK IF THERE IS RENTAL CHARGES. RENTAL MUST BE CONFIRMED BY THE BORROWER
#
my $rentalConfirmation = C4::Context->preference("RentalFeesCheckoutConfirmation");
if ($rentalConfirmation) {
my ($rentalCharge) = GetIssuingCharges( $item_object->itemnumber, $patron->borrowernumber );
my $itemtype_object = Koha::ItemTypes->find( $item_object->effective_itemtype );
if ($itemtype_object) {
my $accumulate_charge = $fees->accumulate_rentalcharge();
if ( $accumulate_charge > 0 ) {
$rentalCharge += $accumulate_charge;
}
}
if ( $rentalCharge > 0 ) {
$needsconfirmation{RENTALCHARGE} = $rentalCharge;
}
}
unless ( $ignore_reserves ) {
# See if the item is on reserve.
my ( $restype, $res ) = C4::Reserves::CheckReserves( $item_object->itemnumber );
if ($restype) {
my $resbor = $res->{'borrowernumber'};
if ( $resbor ne $patron->borrowernumber ) {
my $patron = Koha::Patrons->find( $resbor );
if ( $restype eq "Waiting" )
{
# The item is on reserve and waiting, but has been
# reserved by some other patron.
$needsconfirmation{RESERVE_WAITING} = 1;
$needsconfirmation{'resfirstname'} = $patron->firstname;
$needsconfirmation{'ressurname'} = $patron->surname;
$needsconfirmation{'rescardnumber'} = $patron->cardnumber;
$needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
$needsconfirmation{'resbranchcode'} = $res->{branchcode};
$needsconfirmation{'reswaitingdate'} = $res->{'waitingdate'};
$needsconfirmation{'reserve_id'} = $res->{reserve_id};
}
elsif ( $restype eq "Reserved" ) {
# The item is on reserve for someone else.
$needsconfirmation{RESERVED} = 1;
$needsconfirmation{'resfirstname'} = $patron->firstname;
$needsconfirmation{'ressurname'} = $patron->surname;
$needsconfirmation{'rescardnumber'} = $patron->cardnumber;
$needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
$needsconfirmation{'resbranchcode'} = $patron->branchcode;
$needsconfirmation{'resreservedate'} = $res->{reservedate};
$needsconfirmation{'reserve_id'} = $res->{reserve_id};
}
}
}
}
## CHECK AGE RESTRICTION
my $agerestriction = $biblioitem->agerestriction;
my ($restriction_age, $daysToAgeRestriction) = GetAgeRestriction( $agerestriction, $patron->unblessed );
if ( $daysToAgeRestriction && $daysToAgeRestriction > 0 ) {
if ( C4::Context->preference('AgeRestrictionOverride') ) {
$needsconfirmation{AGE_RESTRICTION} = "$agerestriction";
}
else {
$issuingimpossible{AGE_RESTRICTION} = "$agerestriction";
}
}
## check for high holds decreasing loan period
if ( C4::Context->preference('decreaseLoanHighHolds') ) {
my $check = checkHighHolds( $item_unblessed, $patron_unblessed );
if ( $check->{exceeded} ) {
if ($override_high_holds) {
$alerts{HIGHHOLDS} = {
num_holds => $check->{outstanding},
duration => $check->{duration},
returndate => output_pref( { dt => dt_from_string($check->{due_date}), dateformat => 'iso', timeformat => '24hr' }),
};
}
else {
$needsconfirmation{HIGHHOLDS} = {
num_holds => $check->{outstanding},
duration => $check->{duration},
returndate => output_pref( { dt => dt_from_string($check->{due_date}), dateformat => 'iso', timeformat => '24hr' }),
};
}
}
}
if (
!C4::Context->preference('AllowMultipleIssuesOnABiblio') &&
# don't do the multiple loans per bib check if we've
# already determined that we've got a loan on the same item
!$issuingimpossible{NO_MORE_RENEWALS} &&
!$needsconfirmation{RENEW_ISSUE}
) {
# Check if borrower has already issued an item from the same biblio
# Only if it's not a subscription
my $biblionumber = $item_object->biblionumber;
require C4::Serials;
my $is_a_subscription = C4::Serials::CountSubscriptionFromBiblionumber($biblionumber);
unless ($is_a_subscription) {
# FIXME Should be $patron->checkouts($args);
my $checkouts = Koha::Checkouts->search(
{
borrowernumber => $patron->borrowernumber,
biblionumber => $biblionumber,
},
{
join => 'item',
}
);
# if we get here, we don't already have a loan on this item,
# so if there are any loans on this bib, ask for confirmation
if ( $checkouts->count ) {
$needsconfirmation{BIBLIO_ALREADY_ISSUED} = 1;
}
}
}
return ( \%issuingimpossible, \%needsconfirmation, \%alerts, \%messages, );
}
=head2 CanBookBeReturned
($returnallowed, $message) = CanBookBeReturned($item, $branch)
Check whether the item can be returned to the provided branch
=over 4
=item C<$item> is a hash of item information as returned Koha::Items->find->unblessed (Temporary, should be a Koha::Item instead)
=item C<$branch> is the branchcode where the return is taking place
=back
Returns:
=over 4
=item C<$returnallowed> is 0 or 1, corresponding to whether the return is allowed (1) or not (0)
=item C<$message> is the branchcode where the item SHOULD be returned, if the return is not allowed
=back
=cut
sub CanBookBeReturned {
my ($item, $branch) = @_;
my $allowreturntobranch = C4::Context->preference("AllowReturnToBranch") || 'anywhere';
# assume return is allowed to start
my $allowed = 1;
my $message;
# identify all cases where return is forbidden
if ($allowreturntobranch eq 'homebranch' && $branch ne $item->{'homebranch'}) {
$allowed = 0;
$message = $item->{'homebranch'};
} elsif ($allowreturntobranch eq 'holdingbranch' && $branch ne $item->{'holdingbranch'}) {
$allowed = 0;
$message = $item->{'holdingbranch'};
} elsif ($allowreturntobranch eq 'homeorholdingbranch' && $branch ne $item->{'homebranch'} && $branch ne $item->{'holdingbranch'}) {
$allowed = 0;
$message = $item->{'homebranch'}; # FIXME: choice of homebranch is arbitrary
}
return ($allowed, $message);
}
=head2 CheckHighHolds
used when syspref decreaseLoanHighHolds is active. Returns 1 or 0 to define whether the minimum value held in
decreaseLoanHighHoldsValue is exceeded, the total number of outstanding holds, the number of days the loan
has been decreased to (held in syspref decreaseLoanHighHoldsValue), and the new due date
=cut
sub checkHighHolds {
my ( $item, $borrower ) = @_;
my $branchcode = _GetCircControlBranch( $item, $borrower );
my $item_object = Koha::Items->find( $item->{itemnumber} );
my $return_data = {
exceeded => 0,
outstanding => 0,
duration => 0,
due_date => undef,
};
my $holds = Koha::Holds->search( { biblionumber => $item->{'biblionumber'} } );
if ( $holds->count() ) {
$return_data->{outstanding} = $holds->count();
my $decreaseLoanHighHoldsControl = C4::Context->preference('decreaseLoanHighHoldsControl');
my $decreaseLoanHighHoldsValue = C4::Context->preference('decreaseLoanHighHoldsValue');
my $decreaseLoanHighHoldsIgnoreStatuses = C4::Context->preference('decreaseLoanHighHoldsIgnoreStatuses');
my @decreaseLoanHighHoldsIgnoreStatuses = split( /,/, $decreaseLoanHighHoldsIgnoreStatuses );
if ( $decreaseLoanHighHoldsControl eq 'static' ) {
# static means just more than a given number of holds on the record
# If the number of holds is less than the threshold, we can stop here
if ( $holds->count() < $decreaseLoanHighHoldsValue ) {
return $return_data;
}
}
elsif ( $decreaseLoanHighHoldsControl eq 'dynamic' ) {
# dynamic means X more than the number of holdable items on the record
# let's get the items
my @items = $holds->next()->biblio()->items()->as_list;
# Remove any items with status defined to be ignored even if the would not make item unholdable
foreach my $status (@decreaseLoanHighHoldsIgnoreStatuses) {
@items = grep { !$_->$status } @items;
}
# Remove any items that are not holdable for this patron
@items = grep { CanItemBeReserved( $borrower->{borrowernumber}, $_->itemnumber, undef, { ignore_found_holds => 1 } )->{status} eq 'OK' } @items;
my $items_count = scalar @items;
my $threshold = $items_count + $decreaseLoanHighHoldsValue;
# If the number of holds is less than the count of items we have
# plus the number of holds allowed above that count, we can stop here
if ( $holds->count() <= $threshold ) {
return $return_data;
}
}
my $issuedate = dt_from_string();
my $itype = $item_object->effective_itemtype;
my $daysmode = Koha::CirculationRules->get_effective_daysmode(
{
categorycode => $borrower->{categorycode},
itemtype => $itype,
branchcode => $branchcode,
}
);
my $calendar = Koha::Calendar->new( branchcode => $branchcode, days_mode => $daysmode );
my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branchcode, $borrower );
my $rule = Koha::CirculationRules->get_effective_rule(
{
categorycode => $borrower->{categorycode},
itemtype => $item_object->effective_itemtype,
branchcode => $branchcode,
rule_name => 'decreaseloanholds',
}
);
my $duration;
if ( defined($rule) && $rule->rule_value ne '' ){
# overrides decreaseLoanHighHoldsDuration syspref
$duration = $rule->rule_value;
} else {
$duration = C4::Context->preference('decreaseLoanHighHoldsDuration');
}
my $reduced_datedue = $calendar->addDate( $issuedate, $duration );
$reduced_datedue->set_hour($orig_due->hour);
$reduced_datedue->set_minute($orig_due->minute);
$reduced_datedue->truncate( to => 'minute' );
if ( DateTime->compare( $reduced_datedue, $orig_due ) == -1 ) {
$return_data->{exceeded} = 1;
$return_data->{duration} = $duration;
$return_data->{due_date} = $reduced_datedue;
}
}
return $return_data;
}
=head2 AddIssue
&AddIssue($borrower, $barcode, [$datedue], [$cancelreserve], [$issuedate])
Issue a book. Does no check, they are done in CanBookBeIssued. If we reach this sub, it means the user confirmed if needed.
=over 4
=item C<$borrower> is a hash with borrower informations (from Koha::Patron->unblessed).
=item C<$barcode> is the barcode of the item being issued.
=item C<$datedue> is a DateTime object for the max date of return, i.e. the date due (optional).
Calculated if empty.
=item C<$cancelreserve> is 1 to override and cancel any pending reserves for the item (optional).
=item C<$issuedate> is the date to issue the item in iso (YYYY-MM-DD) format (optional).
Defaults to today. Unlike C<$datedue>, NOT a DateTime object, unfortunately.
AddIssue does the following things :
- step 01: check that there is a borrowernumber & a barcode provided
- check for RENEWAL (book issued & being issued to the same patron)
- renewal YES = Calculate Charge & renew
- renewal NO =
* BOOK ACTUALLY ISSUED ? do a return if book is actually issued (but to someone else)
* RESERVE PLACED ?
- fill reserve if reserve to this patron
- cancel reserve or not, otherwise
* TRANSFERT PENDING ?
- complete the transfert
* ISSUE THE BOOK
=back
=cut
sub AddIssue {
my ( $borrower, $barcode, $datedue, $cancelreserve, $issuedate, $sipmode, $params ) = @_;
my $onsite_checkout = $params && $params->{onsite_checkout} ? 1 : 0;
my $switch_onsite_checkout = $params && $params->{switch_onsite_checkout};
my $auto_renew = $params && $params->{auto_renew};
my $dbh = C4::Context->dbh;
my $barcodecheck = CheckValidBarcode($barcode);
my $issue;
if ( $datedue && ref $datedue ne 'DateTime' ) {
$datedue = dt_from_string($datedue);
}
# $issuedate defaults to today.
if ( !defined $issuedate ) {
$issuedate = dt_from_string();
}
else {
if ( ref $issuedate ne 'DateTime' ) {
$issuedate = dt_from_string($issuedate);
}
}
# Stop here if the patron or barcode doesn't exist
if ( $borrower && $barcode && $barcodecheck ) {
# find which item we issue
my $item_object = Koha::Items->find({ barcode => $barcode })
or return; # if we don't get an Item, abort.
my $item_unblessed = $item_object->unblessed;
my $branchcode = _GetCircControlBranch( $item_unblessed, $borrower );
# get actual issuing if there is one
my $actualissue = $item_object->checkout;
# check if we just renew the issue.
if ( $actualissue and $actualissue->borrowernumber eq $borrower->{'borrowernumber'}
and not $switch_onsite_checkout ) {
$datedue = AddRenewal(
$borrower->{'borrowernumber'},
$item_object->itemnumber,
$branchcode,
$datedue,
$issuedate, # here interpreted as the renewal date
);
}
else {
unless ($datedue) {
my $itype = $item_object->effective_itemtype;
$datedue = CalcDateDue( $issuedate, $itype, $branchcode, $borrower );
}
$datedue->truncate( to => 'minute' );
my $patron = Koha::Patrons->find( $borrower );
my $library = Koha::Libraries->find( $branchcode );
my $fees = Koha::Charges::Fees->new(
{
patron => $patron,
library => $library,
item => $item_object,
to_date => $datedue,
}
);
# it's NOT a renewal
if ( $actualissue and not $switch_onsite_checkout ) {
# This book is currently on loan, but not to the person
# who wants to borrow it now. mark it returned before issuing to the new borrower
my ( $allowed, $message ) = CanBookBeReturned( $item_unblessed, C4::Context->userenv->{branch} );
return unless $allowed;
AddReturn( $item_object->barcode, C4::Context->userenv->{'branch'} );
}
C4::Reserves::MoveReserve( $item_object->itemnumber, $borrower->{'borrowernumber'}, $cancelreserve );
# Starting process for transfer job (checking transfert and validate it if we have one)
if ( my $transfer = $item_object->get_transfer ) {
# updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....)
$transfer->set(
{
datearrived => dt_from_string,
tobranch => C4::Context->userenv->{branch},
comments => 'Forced branchtransfer'
}
)->store;
if ( $transfer->reason && $transfer->reason eq 'Reserve' ) {
my $hold = $item_object->holds->search( { found => 'T' } )->next;
if ( $hold ) { # Is this really needed?
$hold->set( { found => undef } )->store;
C4::Reserves::ModReserveMinusPriority($item_object->itemnumber, $hold->reserve_id);
}
}
}
# If automatic renewal wasn't selected while issuing, set the value according to the issuing rule.
unless ($auto_renew) {
my $rule = Koha::CirculationRules->get_effective_rule(
{
categorycode => $borrower->{categorycode},
itemtype => $item_object->effective_itemtype,
branchcode => $branchcode,
rule_name => 'auto_renew'
}
);
$auto_renew = $rule->rule_value if $rule;
}
my $issue_attributes = {
borrowernumber => $borrower->{'borrowernumber'},
issuedate => $issuedate->strftime('%Y-%m-%d %H:%M:%S'),
date_due => $datedue->strftime('%Y-%m-%d %H:%M:%S'),
branchcode => C4::Context->userenv->{'branch'},
onsite_checkout => $onsite_checkout,
auto_renew => $auto_renew ? 1 : 0,
};
# Get ID of logged in user. if called from a batch job,
# no user session exists and C4::Context->userenv() returns
# the scalar '0'. Only do this if the syspref says so
if ( C4::Context->preference('RecordStaffUserOnCheckout') ) {
my $userenv = C4::Context->userenv();
my $usernumber = (ref($userenv) eq 'HASH') ? $userenv->{'number'} : undef;
if ($usernumber) {
$issue_attributes->{issuer_id} = $usernumber;
}
}
# In the case that the borrower has an on-site checkout
# and SwitchOnSiteCheckouts is enabled this converts it to a regular checkout
$issue = Koha::Checkouts->find( { itemnumber => $item_object->itemnumber } );
if ($issue) {
$issue->set($issue_attributes)->store;
}
else {
$issue = Koha::Checkout->new(
{
itemnumber => $item_object->itemnumber,
%$issue_attributes,
}
)->store;
}
$issue->discard_changes;
if ( $item_object->location && $item_object->location eq 'CART'
&& ( !$item_object->permanent_location || $item_object->permanent_location ne 'CART' ) ) {
## Item was moved to cart via UpdateItemLocationOnCheckin, anything issued should be taken off the cart.
CartToShelf( $item_object->itemnumber );
}
if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) {
UpdateTotalIssues( $item_object->biblionumber, 1 );
}
# Record if item was lost
my $was_lost = $item_object->itemlost;
$item_object->issues( ( $item_object->issues || 0 ) + 1);
$item_object->holdingbranch(C4::Context->userenv->{'branch'});
$item_object->itemlost(0);
$item_object->onloan($datedue->ymd());
$item_object->datelastborrowed( dt_from_string()->ymd() );
$item_object->datelastseen( dt_from_string()->ymd() );
$item_object->store({log_action => 0});
# If the item was lost, it has now been found, charge the overdue if necessary
if ($was_lost) {
if ( $item_object->{_charge} ) {
$actualissue //= Koha::Old::Checkouts->search(
{ itemnumber => $item_unblessed->{itemnumber} },
{
order_by => { '-desc' => 'returndate' },
rows => 1
}
)->single;
unless ( exists( $borrower->{branchcode} ) ) {
my $patron = $actualissue->patron;
$borrower = $patron->unblessed;
}
_CalculateAndUpdateFine(
{
issue => $actualissue,
item => $item_unblessed,
borrower => $borrower,
return_date => $issuedate
}
);
_FixOverduesOnReturn( $borrower->{borrowernumber},
$item_object->itemnumber, undef, 'RENEWED' );
}
}
# If it costs to borrow this book, charge it to the patron's account.
my ( $charge, $itemtype ) = GetIssuingCharges( $item_object->itemnumber, $borrower->{'borrowernumber'} );
if ( $charge && $charge > 0 ) {
AddIssuingCharge( $issue, $charge, 'RENT' );
}
my $itemtype_object = Koha::ItemTypes->find( $item_object->effective_itemtype );
if ( $itemtype_object ) {
my $accumulate_charge = $fees->accumulate_rentalcharge();
if ( $accumulate_charge > 0 ) {
AddIssuingCharge( $issue, $accumulate_charge, 'RENT_DAILY' );
$charge += $accumulate_charge;
$item_unblessed->{charge} = $charge;
}
}
# Record the fact that this book was issued.
&UpdateStats(
{
branch => C4::Context->userenv->{'branch'},
type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
amount => $charge,
other => ( $sipmode ? "SIP-$sipmode" : '' ),
itemnumber => $item_object->itemnumber,
itemtype => $item_object->effective_itemtype,
location => $item_object->location,
borrowernumber => $borrower->{'borrowernumber'},
ccode => $item_object->ccode,
}
);
# Send a checkout slip.
my $circulation_alert = 'C4::ItemCirculationAlertPreference';
my %conditions = (
branchcode => $branchcode,
categorycode => $borrower->{categorycode},
item_type => $item_object->effective_itemtype,
notification => 'CHECKOUT',
);
if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
SendCirculationAlert(
{
type => 'CHECKOUT',
item => $item_object->unblessed,
borrower => $borrower,
branch => $branchcode,
}
);
}
logaction(
"CIRCULATION", "ISSUE",
$borrower->{'borrowernumber'},
$item_object->itemnumber,
) if C4::Context->preference("IssueLog");
Koha::Plugins->call('after_circ_action', {
action => 'checkout',
payload => {
type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
checkout => $issue->get_from_storage
}
});
}
}
return $issue;
}
=head2 GetLoanLength
my $loanlength = &GetLoanLength($borrowertype,$itemtype,branchcode)
Get loan length for an itemtype, a borrower type and a branch
=cut
sub GetLoanLength {
my ( $categorycode, $itemtype, $branchcode ) = @_;
# Initialize default values
my $rules = {
issuelength => 0,
renewalperiod => 0,
lengthunit => 'days',
};
my $found = Koha::CirculationRules->get_effective_rules( {
branchcode => $branchcode,
categorycode => $categorycode,
itemtype => $itemtype,
rules => [
'issuelength',
'renewalperiod',
'lengthunit'
],
} );
# Search for rules!
foreach my $rule_name (keys %$found) {
$rules->{$rule_name} = $found->{$rule_name};
}
return $rules;
}
=head2 GetHardDueDate
my ($hardduedate,$hardduedatecompare) = &GetHardDueDate($borrowertype,$itemtype,branchcode)
Get the Hard Due Date and it's comparison for an itemtype, a borrower type and a branch
=cut
sub GetHardDueDate {
my ( $borrowertype, $itemtype, $branchcode ) = @_;
my $rules = Koha::CirculationRules->get_effective_rules(
{
categorycode => $borrowertype,
itemtype => $itemtype,
branchcode => $branchcode,
rules => [ 'hardduedate', 'hardduedatecompare' ],
}
);
if ( defined( $rules->{hardduedate} ) ) {
if ( $rules->{hardduedate} ) {
return ( dt_from_string( $rules->{hardduedate}, 'iso' ), $rules->{hardduedatecompare} );
}
else {
return ( undef, undef );
}
}
}
=head2 GetBranchBorrowerCircRule
my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
Retrieves circulation rule attributes that apply to the given
branch and patron category, regardless of item type.
The return value is a hashref containing the following key:
patron_maxissueqty - maximum number of loans that a
patron of the given category can have at the given
branch. If the value is undef, no limit.
patron_maxonsiteissueqty - maximum of on-site checkouts that a
patron of the given category can have at the given
branch. If the value is undef, no limit.
This will check for different branch/category combinations in the following order:
branch and category
branch only
category only
default branch and category
If no rule has been found in the database, it will default to
the buillt in rule:
patron_maxissueqty - undef
patron_maxonsiteissueqty - undef
C<$branchcode> and C<$categorycode> should contain the
literal branch code and patron category code, respectively - no
wildcards.
=cut
sub GetBranchBorrowerCircRule {
my ( $branchcode, $categorycode ) = @_;
# Initialize default values
my $rules = {
patron_maxissueqty => undef,
patron_maxonsiteissueqty => undef,
};
# Search for rules!
foreach my $rule_name (qw( patron_maxissueqty patron_maxonsiteissueqty )) {
my $rule = Koha::CirculationRules->get_effective_rule(
{
categorycode => $categorycode,
itemtype => undef,
branchcode => $branchcode,
rule_name => $rule_name,
}
);
$rules->{$rule_name} = $rule->rule_value if defined $rule;
}
return $rules;
}
=head2 GetBranchItemRule
my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
Retrieves circulation rule attributes that apply to the given
branch and item type, regardless of patron category.
The return value is a hashref containing the following keys:
holdallowed => Hold policy for this branch and itemtype. Possible values:
0: No holds allowed.
1: Holds allowed only by patrons that have the same homebranch as the item.
2: Holds allowed from any patron.
returnbranch => branch to which to return item. Possible values:
noreturn: do not return, let item remain where checked in (floating collections)
homebranch: return to item's home branch
holdingbranch: return to issuer branch
This searches branchitemrules in the following order:
* Same branchcode and itemtype
* Same branchcode, itemtype '*'
* branchcode '*', same itemtype
* branchcode and itemtype '*'
Neither C<$branchcode> nor C<$itemtype> should be '*'.
=cut
sub GetBranchItemRule {
my ( $branchcode, $itemtype ) = @_;
# Search for rules!
my $holdallowed_rule = Koha::CirculationRules->get_effective_rule(
{
branchcode => $branchcode,
itemtype => $itemtype,
rule_name => 'holdallowed',
}
);
my $hold_fulfillment_policy_rule = Koha::CirculationRules->get_effective_rule(
{
branchcode => $branchcode,
itemtype => $itemtype,
rule_name => 'hold_fulfillment_policy',
}
);
my $returnbranch_rule = Koha::CirculationRules->get_effective_rule(
{
branchcode => $branchcode,
itemtype => $itemtype,
rule_name => 'returnbranch',
}
);
# built-in default circulation rule
my $rules;
$rules->{holdallowed} = defined $holdallowed_rule
? $holdallowed_rule->rule_value
: 2;
$rules->{hold_fulfillment_policy} = defined $hold_fulfillment_policy_rule
? $hold_fulfillment_policy_rule->rule_value
: 'any';
$rules->{returnbranch} = defined $returnbranch_rule
? $returnbranch_rule->rule_value
: 'homebranch';
return $rules;
}
=head2 AddReturn
($doreturn, $messages, $iteminformation, $borrower) =
&AddReturn( $barcode, $branch [,$exemptfine] [,$returndate] );
Returns a book.
=over 4
=item C<$barcode> is the bar code of the book being returned.
=item C<$branch> is the code of the branch where the book is being returned.
=item C<$exemptfine> indicates that overdue charges for the item will be
removed. Optional.
=item C<$return_date> allows the default return date to be overridden
by the given return date. Optional.
=back
C<&AddReturn> returns a list of four items:
C<$doreturn> is true iff the return succeeded.
C<$messages> is a reference-to-hash giving feedback on the operation.
The keys of the hash are:
=over 4
=item C<BadBarcode>
No item with this barcode exists. The value is C<$barcode>.
=item C<NotIssued>
The book is not currently on loan. The value is C<$barcode>.
=item C<withdrawn>
This book has been withdrawn/cancelled. The value should be ignored.
=item C<Wrongbranch>
This book has was returned to the wrong branch. The value is a hashref
so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
contain the branchcode of the incorrect and correct return library, respectively.
=item C<ResFound>
The item was reserved. The value is a reference-to-hash whose keys are
fields from the reserves table of the Koha database, and
C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
either C<Waiting>, C<Reserved>, or 0.
=item C<WasReturned>
Value 1 if return is successful.
=item C<NeedsTransfer>
If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer.
=back
C<$iteminformation> is a reference-to-hash, giving information about the
returned item from the issues table.
C<$borrower> is a reference-to-hash, giving information about the
patron who last borrowed the book.
=cut
sub AddReturn {
my ( $barcode, $branch, $exemptfine, $return_date ) = @_;
if ($branch and not Koha::Libraries->find($branch)) {
warn "AddReturn error: branch '$branch' not found. Reverting to " . C4::Context->userenv->{'branch'};
undef $branch;
}
$branch = C4::Context->userenv->{'branch'} unless $branch; # we trust userenv to be a safe fallback/default
my $return_date_specified = !!$return_date;
$return_date //= dt_from_string();
my $messages;
my $patron;
my $doreturn = 1;
my $validTransfer = 1;
my $stat_type = 'return';
# get information on item
my $item = Koha::Items->find({ barcode => $barcode });
unless ($item) {
return ( 0, { BadBarcode => $barcode } ); # no barcode means no item or borrower. bail out.
}
my $itemnumber = $item->itemnumber;
my $itemtype = $item->effective_itemtype;
my $issue = $item->checkout;
if ( $issue ) {
$patron = $issue->patron
or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existent borrowernumber '" . $issue->borrowernumber . "'\n"
. Dumper($issue->unblessed) . "\n";
} else {
$messages->{'NotIssued'} = $barcode;
$item->onloan(undef)->store({skip_record_index=>1}) if defined $item->onloan;
# even though item is not on loan, it may still be transferred; therefore, get current branch info
$doreturn = 0;
# No issue, no borrowernumber. ONLY if $doreturn, *might* you have a $borrower later.
# Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
if (C4::Context->preference("RecordLocalUseOnReturn")) {
$messages->{'LocalUse'} = 1;
$stat_type = 'localuse';
}
}
# full item data, but no borrowernumber or checkout info (no issue)
my $hbr = GetBranchItemRule($item->homebranch, $itemtype)->{'returnbranch'} || "homebranch";
# get the proper branch to which to return the item
my $returnbranch = $hbr ne 'noreturn' ? $item->$hbr : $branch;
# if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
my $transfer_trigger = $hbr eq 'homebranch' ? 'ReturnToHome' : $hbr eq 'holdingbranch' ? 'ReturnToHolding' : undef;
my $borrowernumber = $patron ? $patron->borrowernumber : undef; # we don't know if we had a borrower or not
my $patron_unblessed = $patron ? $patron->unblessed : {};
my $update_loc_rules = get_yaml_pref_hash('UpdateItemLocationOnCheckin');
map { $update_loc_rules->{$_} = $update_loc_rules->{$_}[0] } keys %$update_loc_rules; #We can only move to one location so we flatten the arrays
if ($update_loc_rules) {
if (defined $update_loc_rules->{_ALL_}) {
if ($update_loc_rules->{_ALL_} eq '_PERM_') { $update_loc_rules->{_ALL_} = $item->permanent_location; }
if ($update_loc_rules->{_ALL_} eq '_BLANK_') { $update_loc_rules->{_ALL_} = ''; }
if ( defined $item->location && $item->location ne $update_loc_rules->{_ALL_}) {
$messages->{'ItemLocationUpdated'} = { from => $item->location, to => $update_loc_rules->{_ALL_} };
$item->location($update_loc_rules->{_ALL_})->store({skip_record_index=>1});
}
}
else {
foreach my $key ( keys %$update_loc_rules ) {
if ( $update_loc_rules->{$key} eq '_PERM_' ) { $update_loc_rules->{$key} = $item->permanent_location; }
if ( $update_loc_rules->{$key} eq '_BLANK_') { $update_loc_rules->{$key} = '' ;}
if ( ($item->location eq $key && $item->location ne $update_loc_rules->{$key}) || ($key eq '_BLANK_' && $item->location eq '' && $update_loc_rules->{$key} ne '') ) {
$messages->{'ItemLocationUpdated'} = { from => $item->location, to => $update_loc_rules->{$key} };
$item->location($update_loc_rules->{$key})->store({skip_record_index=>1});
last;
}
}
}
}
my $yaml = C4::Context->preference('UpdateNotForLoanStatusOnCheckin');
if ($yaml) {
$yaml = "$yaml\n\n"; # YAML is anal on ending \n. Surplus does not hurt
my $rules;
eval { $rules = YAML::Load($yaml); };
if ($@) {
warn "Unable to parse UpdateNotForLoanStatusOnCheckin syspref : $@";
}
else {
foreach my $key ( keys %$rules ) {
if ( $item->notforloan eq $key ) {
$messages->{'NotForLoanStatusUpdated'} = { from => $item->notforloan, to => $rules->{$key} };
$item->notforloan($rules->{$key})->store({ log_action => 0, skip_record_index => 1 });
last;
}
}
}
}
# check if the return is allowed at this branch
my ($returnallowed, $message) = CanBookBeReturned($item->unblessed, $branch);
unless ($returnallowed){
$messages->{'Wrongbranch'} = {
Wrongbranch => $branch,
Rightbranch => $message
};
$doreturn = 0;
my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
$indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
return ( $doreturn, $messages, $issue, $patron_unblessed);
}
if ( $item->withdrawn ) { # book has been cancelled
$messages->{'withdrawn'} = 1;
$doreturn = 0 if C4::Context->preference("BlockReturnOfWithdrawnItems");
}
if ( $item->itemlost and C4::Context->preference("BlockReturnOfLostItems") ) {
$doreturn = 0;
}
# case of a return of document (deal with issues and holdingbranch)
if ($doreturn) {
die "The item is not issed and cannot be returned" unless $issue; # Just in case...
$patron or warn "AddReturn without current borrower";
if ($patron) {
eval {
MarkIssueReturned( $borrowernumber, $item->itemnumber, $return_date, $patron->privacy, { skip_record_index => 1} );
};
unless ( $@ ) {
if (
(
C4::Context->preference('CalculateFinesOnReturn')
|| ( $return_date_specified && C4::Context->preference('CalculateFinesOnBackdate') )
)
&& !$item->itemlost
)
{
_CalculateAndUpdateFine( { issue => $issue, item => $item->unblessed, borrower => $patron_unblessed, return_date => $return_date } );
}
} else {
carp "The checkin for the following issue failed, Please go to the about page, section 'data corrupted' to know how to fix this problem ($@)" . Dumper( $issue->unblessed );
my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
$indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
return ( 0, { WasReturned => 0, DataCorrupted => 1 }, $issue, $patron_unblessed );
}
# FIXME is the "= 1" right? This could be the borrower hash.
$messages->{'WasReturned'} = 1;
} else {
$item->onloan(undef)->store({ log_action => 0 , skip_record_index => 1 });
}
}
# the holdingbranch is updated if the document is returned to another location.
# this is always done regardless of whether the item was on loan or not
if ($item->holdingbranch ne $branch) {
$item->holdingbranch($branch)->store({ skip_record_index => 1 });
}
my $item_was_lost = $item->itemlost;
my $leave_item_lost = C4::Context->preference("BlockReturnOfLostItems") ? 1 : 0;
my $updated_item = ModDateLastSeen( $item->itemnumber, $leave_item_lost, { skip_record_index => 1 } ); # will unset itemlost if needed
# fix up the accounts.....
if ($item_was_lost) {
$messages->{'WasLost'} = 1;
unless ( C4::Context->preference("BlockReturnOfLostItems") ) {
$messages->{'LostItemFeeRefunded'} = $updated_item->{_refunded};
$messages->{'LostItemFeeRestored'} = $updated_item->{_restored};
if ( $updated_item->{_charge} ) {
$issue //= Koha::Old::Checkouts->search(
{ itemnumber => $item->itemnumber },
{ order_by => { '-desc' => 'returndate' }, rows => 1 } )
->single;
unless ( exists( $patron_unblessed->{branchcode} ) ) {
my $patron = $issue->patron;
$patron_unblessed = $patron->unblessed;
}
_CalculateAndUpdateFine(
{
issue => $issue,
item => $item->unblessed,
borrower => $patron_unblessed,
return_date => $return_date
}
);
_FixOverduesOnReturn( $patron_unblessed->{borrowernumber},
$item->itemnumber, undef, 'RETURNED' );
$messages->{'LostItemFeeCharged'} = 1;
}
}
}
# check if we have a transfer for this document
my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->itemnumber );
# if we have a transfer to complete, we update the line of transfers with the datearrived
my $is_in_rotating_collection = C4::RotatingCollections::isItemInAnyCollection( $item->itemnumber );
if ($datesent) {
# At this point we will either fill the transfer or it is a wrong transfer
# either way we should not now generate a new transfer
$validTransfer = 0;
if ( $tobranch eq $branch ) {
my $sth = C4::Context->dbh->prepare(
"UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL"
);
$sth->execute( $item->itemnumber );
$messages->{'TransferArrived'} = $frombranch;
} else {
$messages->{'WrongTransfer'} = $tobranch;
$messages->{'WrongTransferItem'} = $item->itemnumber;
}
}
# fix up the overdues in accounts...
if ($borrowernumber) {
my $fix = _FixOverduesOnReturn( $borrowernumber, $item->itemnumber, $exemptfine, 'RETURNED' );
defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, ".$item->itemnumber."...) failed!"; # zero is OK, check defined
if ( $issue and $issue->is_overdue($return_date) ) {
# fix fine days
my ($debardate,$reminder) = _debar_user_on_return( $patron_unblessed, $item->unblessed, dt_from_string($issue->date_due), $return_date );
if ($reminder){
$messages->{'PrevDebarred'} = $debardate;
} else {
$messages->{'Debarred'} = $debardate if $debardate;
}
# there's no overdue on the item but borrower had been previously debarred
} elsif ( $issue->date_due and $patron->debarred ) {
if ( $patron->debarred eq "9999-12-31") {
$messages->{'ForeverDebarred'} = $patron->debarred;
} else {
my $borrower_debar_dt = dt_from_string( $patron->debarred );
$borrower_debar_dt->truncate(to => 'day');
my $today_dt = $return_date->clone()->truncate(to => 'day');
if ( DateTime->compare( $borrower_debar_dt, $today_dt ) != -1 ) {
$messages->{'PrevDebarred'} = $patron->debarred;
}
}
}
}
# find reserves.....
# launch the Checkreserves routine to find any holds
my ($resfound, $resrec);
my $lookahead= C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->itemnumber, undef, $lookahead ) unless ( $item->withdrawn );
# if a hold is found and is waiting at another branch, change the priority back to 1 and trigger the hold (this will trigger a transfer and update the hold status properly)
if ( $resfound and $resfound eq "Waiting" and $branch ne $resrec->{branchcode} ) {
my $hold = C4::Reserves::RevertWaitingStatus( { itemnumber => $item->itemnumber } );
$resfound = 'Reserved';
$resrec = $hold->unblessed;
}
if ($resfound) {
$resrec->{'ResFound'} = $resfound;
$messages->{'ResFound'} = $resrec;
}
# Record the fact that this book was returned.
UpdateStats({
branch => $branch,
type => $stat_type,
itemnumber => $itemnumber,
itemtype => $itemtype,
location => $item->location,
borrowernumber => $borrowernumber,
ccode => $item->ccode,
});
# Send a check-in slip. # NOTE: borrower may be undef. Do not try to send messages then.
if ( $patron ) {
my $circulation_alert = 'C4::ItemCirculationAlertPreference';
my %conditions = (
branchcode => $branch,
categorycode => $patron->categorycode,
item_type => $itemtype,
notification => 'CHECKIN',
);
if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
SendCirculationAlert({
type => 'CHECKIN',
item => $item->unblessed,
borrower => $patron->unblessed,
branch => $branch,
});
}
logaction("CIRCULATION", "RETURN", $borrowernumber, $item->itemnumber)
if C4::Context->preference("ReturnLog");
}
# Check if this item belongs to a biblio record that is attached to an
# ILL request, if it is we need to update the ILL request's status
if ( $doreturn and C4::Context->preference('CirculateILL')) {
my $request = Koha::Illrequests->find(
{ biblio_id => $item->biblio->biblionumber }
);
$request->status('RET') if $request;
}
# Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
if ($validTransfer && !$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) ){
my $BranchTransferLimitsType = C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ? 'effective_itemtype' : 'ccode';
if (C4::Context->preference("AutomaticItemReturn" ) or
(C4::Context->preference("UseBranchTransferLimits") and
! IsBranchTransferAllowed($branch, $returnbranch, $item->$BranchTransferLimitsType )
)) {
$debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s, %s)", $item->itemnumber,$branch, $returnbranch, $transfer_trigger;
$debug and warn "item: " . Dumper($item->unblessed);
ModItemTransfer($item->itemnumber, $branch, $returnbranch, $transfer_trigger, { skip_record_index => 1 });
$messages->{'WasTransfered'} = 1;
} else {
$messages->{'NeedsTransfer'} = $returnbranch;
$messages->{'TransferTrigger'} = $transfer_trigger;
}
}
if ( C4::Context->preference('ClaimReturnedLostValue') ) {
my $claims = Koha::Checkouts::ReturnClaims->search(
{
itemnumber => $item->id,
resolution => undef,
}
);
if ( $claims->count ) {
$messages->{ReturnClaims} = $claims;
}
}
my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
$indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
if ( $doreturn and $issue ) {
my $checkin = Koha::Old::Checkouts->find($issue->id);
Koha::Plugins->call('after_circ_action', {
action => 'checkin',
payload => {
checkout=> $checkin
}
});
}
return ( $doreturn, $messages, $issue, ( $patron ? $patron->unblessed : {} ));
}
=head2 MarkIssueReturned
MarkIssueReturned($borrowernumber, $itemnumber, $returndate, $privacy, [$params] );
Unconditionally marks an issue as being returned by
moving the C<issues> row to C<old_issues> and
setting C<returndate> to the current date.
if C<$returndate> is specified (in iso format), it is used as the date
of the return.
C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
the old_issue is immediately anonymised
Ideally, this function would be internal to C<C4::Circulation>,
not exported, but it is currently used in misc/cronjobs/longoverdue.pl
and offline_circ/process_koc.pl.
The last optional parameter allos passing skip_record_index to the item store call.
=cut
sub MarkIssueReturned {
my ( $borrowernumber, $itemnumber, $returndate, $privacy, $params ) = @_;
# Retrieve the issue
my