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.
 
 
 
 
 
 

3800 lines
130 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 2 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use strict;
#use warnings; FIXME - Bug 2505
use DateTime;
use C4::Context;
use C4::Stats;
use C4::Reserves;
use C4::Biblio;
use C4::Items;
use C4::Members;
use C4::Dates;
use C4::Dates qw(format_date);
use C4::Accounts;
use C4::ItemCirculationAlertPreference;
use C4::Message;
use C4::Debug;
use C4::Branch; # GetBranches
use C4::Log; # logaction
use C4::Koha qw(
GetAuthorisedValueByCode
GetAuthValCode
GetKohaAuthorisedValueLib
);
use C4::Overdues qw(CalcFine UpdateFine);
use C4::RotatingCollections qw(GetCollectionItemBranches);
use Algorithm::CheckDigits;
use Data::Dumper;
use Koha::DateUtils;
use Koha::Calendar;
use Koha::Borrower::Debarments;
use Carp;
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($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
BEGIN {
require Exporter;
$VERSION = 3.07.00.049; # for version checking
@ISA = qw(Exporter);
# FIXME subs that should probably be elsewhere
push @EXPORT, qw(
&barcodedecode
&LostItem
&ReturnLostItem
);
# subs to deal with issuing a book
push @EXPORT, qw(
&CanBookBeIssued
&CanBookBeRenewed
&AddIssue
&AddRenewal
&GetRenewCount
&GetSoonestRenewDate
&GetItemIssue
&GetItemIssues
&GetIssuingCharges
&GetIssuingRule
&GetBranchBorrowerCircRule
&GetBranchItemRule
&GetBiblioIssues
&GetOpenIssue
&AnonymiseIssueHistory
&CheckIfIssuedToPatron
&IsItemIssued
);
# 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 stocktaking.
=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::Branch::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($newbranch,
$barcode, $ignore_reserves);
Transfers an item to a new branch. If the item is currently on loan, it is automatically returned before the actual transfer.
C<$newbranch> 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.
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<IsPermanent>
The item's home branch is permanent. This doesn't prevent the item from being transferred, though. The value is the code of the item's home branch.
=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 ( $tbr, $barcode, $ignoreRs ) = @_;
my $messages;
my $dotransfer = 1;
my $branches = GetBranches();
my $itemnumber = GetItemnumberFromBarcode( $barcode );
my $issue = GetItemIssue($itemnumber);
my $biblio = GetBiblioFromItemNumber($itemnumber);
# bad barcode..
if ( not $itemnumber ) {
$messages->{'BadBarcode'} = $barcode;
$dotransfer = 0;
}
# get branches of book...
my $hbr = $biblio->{'homebranch'};
my $fbr = $biblio->{'holdingbranch'};
# if using Branch Transfer Limits
if ( C4::Context->preference("UseBranchTransferLimits") == 1 ) {
if ( C4::Context->preference("item-level_itypes") && C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ) {
if ( ! IsBranchTransferAllowed( $tbr, $fbr, $biblio->{'itype'} ) ) {
$messages->{'NotAllowed'} = $tbr . "::" . $biblio->{'itype'};
$dotransfer = 0;
}
} elsif ( ! IsBranchTransferAllowed( $tbr, $fbr, $biblio->{ C4::Context->preference("BranchTransferLimitsType") } ) ) {
$messages->{'NotAllowed'} = $tbr . "::" . $biblio->{ C4::Context->preference("BranchTransferLimitsType") };
$dotransfer = 0;
}
}
# if is permanent...
if ( $hbr && $branches->{$hbr}->{'PE'} ) {
$messages->{'IsPermanent'} = $hbr;
$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...
if ($issue->{borrowernumber}) {
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 );
# don't need to update MARC anymore, we do it in batch now
$messages->{'WasTransfered'} = 1;
}
ModDateLastSeen( $itemnumber );
return ( $dotransfer, $messages, $biblio );
}
sub TooMany {
my $borrower = shift;
my $biblionumber = shift;
my $item = shift;
my $cat_borrower = $borrower->{'categorycode'};
my $dbh = C4::Context->dbh;
my $branch;
# Get which branchcode we need
$branch = _GetCircControlBranch($item,$borrower);
my $type = (C4::Context->preference('item-level_itypes'))
? $item->{'itype'} # item-level
: $item->{'itemtype'}; # biblio-level
# given branch, patron category, and item type, determine
# applicable issuing rule
my $issuing_rule = GetIssuingRule($cat_borrower, $type, $branch);
# 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($issuing_rule) and defined($issuing_rule->{'maxissueqty'})) {
my @bind_params;
my $count_query = "SELECT COUNT(*) FROM issues
JOIN items USING (itemnumber) ";
my $rule_itemtype = $issuing_rule->{itemtype};
if ($rule_itemtype eq "*") {
# matching rule has the default item type, so count only
# those existing loans that don't fall under a more
# specific rule
if (C4::Context->preference('item-level_itypes')) {
$count_query .= " WHERE items.itype NOT IN (
SELECT itemtype FROM issuingrules
WHERE branchcode = ?
AND (categorycode = ? OR categorycode = ?)
AND itemtype <> '*'
) ";
} else {
$count_query .= " JOIN biblioitems USING (biblionumber)
WHERE biblioitems.itemtype NOT IN (
SELECT itemtype FROM issuingrules
WHERE branchcode = ?
AND (categorycode = ? OR categorycode = ?)
AND itemtype <> '*'
) ";
}
push @bind_params, $issuing_rule->{branchcode};
push @bind_params, $issuing_rule->{categorycode};
push @bind_params, $cat_borrower;
} else {
# rule has specific item type, so count loans of that
# specific item type
if (C4::Context->preference('item-level_itypes')) {
$count_query .= " WHERE items.itype = ? ";
} else {
$count_query .= " JOIN biblioitems USING (biblionumber)
WHERE biblioitems.itemtype= ? ";
}
push @bind_params, $type;
}
$count_query .= " AND borrowernumber = ? ";
push @bind_params, $borrower->{'borrowernumber'};
my $rule_branch = $issuing_rule->{branchcode};
if ($rule_branch ne "*") {
if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
$count_query .= " AND issues.branchcode = ? ";
push @bind_params, $branch;
} elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
; # if branch is the patron's home branch, then count all loans by patron
} else {
$count_query .= " AND items.homebranch = ? ";
push @bind_params, $branch;
}
}
my $count_sth = $dbh->prepare($count_query);
$count_sth->execute(@bind_params);
my ($current_loan_count) = $count_sth->fetchrow_array;
my $max_loans_allowed = $issuing_rule->{'maxissueqty'};
if ($current_loan_count >= $max_loans_allowed) {
return ($current_loan_count, $max_loans_allowed);
}
}
# 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->{maxissueqty})) {
my @bind_params = ();
my $branch_count_query = "SELECT COUNT(*) FROM issues
JOIN items USING (itemnumber)
WHERE borrowernumber = ? ";
push @bind_params, $borrower->{borrowernumber};
if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
$branch_count_query .= " AND issues.branchcode = ? ";
push @bind_params, $branch;
} elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
; # if branch is the patron's home branch, then count all loans by patron
} else {
$branch_count_query .= " AND items.homebranch = ? ";
push @bind_params, $branch;
}
my $branch_count_sth = $dbh->prepare($branch_count_query);
$branch_count_sth->execute(@bind_params);
my ($current_loan_count) = $branch_count_sth->fetchrow_array;
my $max_loans_allowed = $branch_borrower_circ_rule->{maxissueqty};
if ($current_loan_count >= $max_loans_allowed) {
return ($current_loan_count, $max_loans_allowed);
}
}
# OK, the patron can issue !!!
return;
}
=head2 itemissues
@issues = &itemissues($biblioitemnumber, $biblio);
Looks up information about who has borrowed the bookZ<>(s) with the
given biblioitemnumber.
C<$biblio> is ignored.
C<&itemissues> returns an array of references-to-hash. The keys
include the fields from the C<items> table in the Koha database.
Additional keys include:
=over 4
=item C<date_due>
If the item is currently on loan, this gives the due date.
If the item is not on loan, then this is either "Available" or
"Cancelled", if the item has been withdrawn.
=item C<card>
If the item is currently on loan, this gives the card number of the
patron who currently has the item.
=item C<timestamp0>, C<timestamp1>, C<timestamp2>
These give the timestamp for the last three times the item was
borrowed.
=item C<card0>, C<card1>, C<card2>
The card number of the last three patrons who borrowed this item.
=item C<borrower0>, C<borrower1>, C<borrower2>
The borrower number of the last three patrons who borrowed this item.
=back
=cut
#'
sub itemissues {
my ( $bibitem, $biblio ) = @_;
my $dbh = C4::Context->dbh;
my $sth =
$dbh->prepare("Select * from items where items.biblioitemnumber = ?")
|| die $dbh->errstr;
my $i = 0;
my @results;
$sth->execute($bibitem) || die $sth->errstr;
while ( my $data = $sth->fetchrow_hashref ) {
# Find out who currently has this item.
# FIXME - Wouldn't it be better to do this as a left join of
# some sort? Currently, this code assumes that if
# fetchrow_hashref() fails, then the book is on the shelf.
# fetchrow_hashref() can fail for any number of reasons (e.g.,
# database server crash), not just because no items match the
# search criteria.
my $sth2 = $dbh->prepare(
"SELECT * FROM issues
LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
WHERE itemnumber = ?
"
);
$sth2->execute( $data->{'itemnumber'} );
if ( my $data2 = $sth2->fetchrow_hashref ) {
$data->{'date_due'} = $data2->{'date_due'};
$data->{'card'} = $data2->{'cardnumber'};
$data->{'borrower'} = $data2->{'borrowernumber'};
}
else {
$data->{'date_due'} = ($data->{'withdrawn'} eq '1') ? 'Cancelled' : 'Available';
}
# Find the last 3 people who borrowed this item.
$sth2 = $dbh->prepare(
"SELECT * FROM old_issues
LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
WHERE itemnumber = ?
ORDER BY returndate DESC,timestamp DESC"
);
$sth2->execute( $data->{'itemnumber'} );
for ( my $i2 = 0 ; $i2 < 2 ; $i2++ )
{ # FIXME : error if there is less than 3 pple borrowing this item
if ( my $data2 = $sth2->fetchrow_hashref ) {
$data->{"timestamp$i2"} = $data2->{'timestamp'};
$data->{"card$i2"} = $data2->{'cardnumber'};
$data->{"borrower$i2"} = $data2->{'borrowernumber'};
} # if
} # for
$results[$i] = $data;
$i++;
}
return (@results);
}
=head2 CanBookBeIssued
( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $borrower,
$barcode, $duedatespec, $inprocess, $ignore_reserves );
Check if a book can be issued.
C<$issuingimpossible> and C<$needsconfirmation> are some hashref.
=over 4
=item C<$borrower> hash with borrower informations (from GetMember or GetMemberDetails)
=item C<$barcode> is the bar code of the book being issued.
=item C<$duedatespec> is a C4::Dates object.
=item C<$inprocess> boolean switch
=item C<$ignore_reserves> boolean switch
=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 ( $borrower, $barcode, $duedate, $inprocess, $ignore_reserves ) = @_;
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 $item = GetItem(GetItemnumberFromBarcode( $barcode ));
my $issue = GetItemIssue($item->{itemnumber});
my $biblioitem = GetBiblioItemData($item->{biblioitemnumber});
$item->{'itemtype'}=$item->{'itype'};
my $dbh = C4::Context->dbh;
# MANDATORY CHECKS - unless item exists, nothing else matters
unless ( $item->{barcode} ) {
$issuingimpossible{UNKNOWN_BARCODE} = 1;
}
return ( \%issuingimpossible, \%needsconfirmation ) if %issuingimpossible;
#
# DUE DATE is OK ? -- should already have checked.
#
if ($duedate && ref $duedate ne 'DateTime') {
$duedate = dt_from_string($duedate);
}
my $now = DateTime->now( time_zone => C4::Context->tz() );
unless ( $duedate ) {
my $issuedate = $now->clone();
my $branch = _GetCircControlBranch($item,$borrower);
my $itype = ( C4::Context->preference('item-level_itypes') ) ? $item->{'itype'} : $biblioitem->{'itemtype'};
$duedate = CalcDateDue( $issuedate, $itype, $branch, $borrower );
# Offline circ calls AddIssue directly, doesn't run through here
# So issuingimpossible should be ok.
}
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 ( $borrower->{'category_type'} eq 'X' && ( $item->{barcode} )) {
# stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1 .
&UpdateStats({
branch => C4::Context->userenv->{'branch'},
type => 'localuse',
itemnumber => $item->{'itemnumber'},
itemtype => $item->{'itemtype'},
borrowernumber => $borrower->{'borrowernumber'},
ccode => $item->{'ccode'}}
);
ModDateLastSeen( $item->{'itemnumber'} );
return( { STATS => 1 }, {});
}
if ( $borrower->{flags}->{GNA} ) {
$issuingimpossible{GNA} = 1;
}
if ( $borrower->{flags}->{'LOST'} ) {
$issuingimpossible{CARD_LOST} = 1;
}
if ( $borrower->{flags}->{'DBARRED'} ) {
$issuingimpossible{DEBARRED} = 1;
}
if ( !defined $borrower->{dateexpiry} || $borrower->{'dateexpiry'} eq '0000-00-00') {
$issuingimpossible{EXPIRED} = 1;
} else {
my ($y, $m, $d) = split /-/,$borrower->{'dateexpiry'};
if ($y && $m && $d) { # are we really writing oinvalid dates to borrs
my $expiry_dt = DateTime->new(
year => $y,
month => $m,
day => $d,
time_zone => C4::Context->tz,
);
$expiry_dt->truncate( to => 'day');
my $today = $now->clone()->truncate(to => 'day');
if (DateTime->compare($today, $expiry_dt) == 1) {
$issuingimpossible{EXPIRED} = 1;
}
} else {
carp("Invalid expity date in borr");
$issuingimpossible{EXPIRED} = 1;
}
}
#
# BORROWER STATUS
#
# DEBTS
my ($balance, $non_issue_charges, $other_charges) =
C4::Members::GetMemberAccountBalance( $borrower->{'borrowernumber'} );
my $amountlimit = C4::Context->preference("noissuescharge");
my $allowfineoverride = C4::Context->preference("AllowFineOverride");
my $allfinesneedoverride = C4::Context->preference("AllFinesNeedOverride");
if ( C4::Context->preference("IssuingInProcess") ) {
if ( $non_issue_charges > $amountlimit && !$inprocess && !$allowfineoverride) {
$issuingimpossible{DEBT} = sprintf( "%.2f", $non_issue_charges );
} elsif ( $non_issue_charges > $amountlimit && !$inprocess && $allowfineoverride) {
$needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
} elsif ( $allfinesneedoverride && $non_issue_charges > 0 && $non_issue_charges <= $amountlimit && !$inprocess ) {
$needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
}
}
else {
if ( $non_issue_charges > $amountlimit && $allowfineoverride ) {
$needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
} elsif ( $non_issue_charges > $amountlimit && !$allowfineoverride) {
$issuingimpossible{DEBT} = sprintf( "%.2f", $non_issue_charges );
} elsif ( $non_issue_charges > 0 && $allfinesneedoverride ) {
$needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
}
}
if ($balance > 0 && $other_charges > 0) {
$alerts{OTHER_CHARGES} = sprintf( "%.2f", $other_charges );
}
my ($blocktype, $count) = C4::Members::IsMemberBlocked($borrower->{'borrowernumber'});
if ($blocktype == -1) {
## patron has outstanding overdue loans
if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){
$issuingimpossible{USERBLOCKEDOVERDUE} = $count;
}
elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){
$needsconfirmation{USERBLOCKEDOVERDUE} = $count;
}
} elsif($blocktype == 1) {
# patron has accrued fine days or has a restriction. $count is a date
if ($count eq '9999-12-31') {
$issuingimpossible{USERBLOCKEDNOENDDATE} = $count;
}
else {
$issuingimpossible{USERBLOCKEDWITHENDDATE} = $count;
}
}
#
# JB34 CHECKS IF BORROWERS DONT HAVE ISSUE TOO MANY BOOKS
#
my ($current_loan_count, $max_loans_allowed) = TooMany( $borrower, $item->{biblionumber}, $item );
# if TooMany max_loans_allowed returns 0 the user doesn't have permission to check out this book
if (defined $max_loans_allowed && $max_loans_allowed == 0) {
$needsconfirmation{PATRON_CANT} = 1;
} else {
if($max_loans_allowed){
if ( C4::Context->preference("AllowTooManyOverride") ) {
$needsconfirmation{TOO_MANY} = 1;
$needsconfirmation{current_loan_count} = $current_loan_count;
$needsconfirmation{max_loans_allowed} = $max_loans_allowed;
} else {
$issuingimpossible{TOO_MANY} = 1;
$issuingimpossible{current_loan_count} = $current_loan_count;
$issuingimpossible{max_loans_allowed} = $max_loans_allowed;
}
}
}
#
# ITEM CHECKING
#
if ( $item->{'notforloan'} )
{
if(!C4::Context->preference("AllowNotForLoanOverride")){
$issuingimpossible{NOT_FOR_LOAN} = 1;
$issuingimpossible{item_notforloan} = $item->{'notforloan'};
}else{
$needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
$needsconfirmation{item_notforloan} = $item->{'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($item->{'itemtype'});
my $notforloan=$sth->fetchrow_hashref();
if ($notforloan->{'notforloan'}) {
if (!C4::Context->preference("AllowNotForLoanOverride")) {
$issuingimpossible{NOT_FOR_LOAN} = 1;
$issuingimpossible{itemtype_notforloan} = $item->{'itype'};
} else {
$needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
$needsconfirmation{itemtype_notforloan} = $item->{'itype'};
}
}
}
elsif ($biblioitem->{'notforloan'} == 1){
if (!C4::Context->preference("AllowNotForLoanOverride")) {
$issuingimpossible{NOT_FOR_LOAN} = 1;
$issuingimpossible{itemtype_notforloan} = $biblioitem->{'itemtype'};
} else {
$needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
$needsconfirmation{itemtype_notforloan} = $biblioitem->{'itemtype'};
}
}
}
if ( $item->{'withdrawn'} && $item->{'withdrawn'} > 0 )
{
$issuingimpossible{WTHDRAWN} = 1;
}
if ( $item->{'restricted'}
&& $item->{'restricted'} == 1 )
{
$issuingimpossible{RESTRICTED} = 1;
}
if ( $item->{'itemlost'} && C4::Context->preference("IssueLostItem") ne 'nothing' ) {
my $code = GetAuthorisedValueByCode( 'LOST', $item->{'itemlost'} );
$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() ) {
if ( $item->{C4::Context->preference("HomeOrHoldingBranch")} ne $userenv->{branch} ){
$issuingimpossible{ITEMNOTSAMEBRANCH} = 1;
$issuingimpossible{'itemhomebranch'} = $item->{C4::Context->preference("HomeOrHoldingBranch")};
}
$needsconfirmation{BORRNOTSAMEBRANCH} = GetBranchName( $borrower->{'branchcode'} )
if ( $borrower->{'branchcode'} ne $userenv->{branch} );
}
}
#
# CHECK IF BOOK ALREADY ISSUED TO THIS BORROWER
#
if ( $issue->{borrowernumber} && $issue->{borrowernumber} eq $borrower->{'borrowernumber'} )
{
# Already issued to current borrower. Ask whether the loan should
# be renewed.
my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed(
$borrower->{'borrowernumber'},
$item->{'itemnumber'}
);
if ( $CanBookBeRenewed == 0 ) { # no more renewals allowed
$issuingimpossible{NO_MORE_RENEWALS} = 1;
}
else {
$needsconfirmation{RENEW_ISSUE} = 1;
}
}
elsif ($issue->{borrowernumber}) {
# issued to someone else
my $currborinfo = C4::Members::GetMember( borrowernumber => $issue->{borrowernumber} );
# warn "=>.$currborinfo->{'firstname'} $currborinfo->{'surname'} ($currborinfo->{'cardnumber'})";
$needsconfirmation{ISSUED_TO_ANOTHER} = 1;
$needsconfirmation{issued_firstname} = $currborinfo->{'firstname'};
$needsconfirmation{issued_surname} = $currborinfo->{'surname'};
$needsconfirmation{issued_cardnumber} = $currborinfo->{'cardnumber'};
$needsconfirmation{issued_borrowernumber} = $currborinfo->{'borrowernumber'};
}
unless ( $ignore_reserves ) {
# See if the item is on reserve.
my ( $restype, $res ) = C4::Reserves::CheckReserves( $item->{'itemnumber'} );
if ($restype) {
my $resbor = $res->{'borrowernumber'};
if ( $resbor ne $borrower->{'borrowernumber'} ) {
my ( $resborrower ) = C4::Members::GetMember( borrowernumber => $resbor );
my $branchname = GetBranchName( $res->{'branchcode'} );
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'} = $resborrower->{'firstname'};
$needsconfirmation{'ressurname'} = $resborrower->{'surname'};
$needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'};
$needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'};
$needsconfirmation{'resbranchname'} = $branchname;
$needsconfirmation{'reswaitingdate'} = format_date($res->{'waitingdate'});
}
elsif ( $restype eq "Reserved" ) {
# The item is on reserve for someone else.
$needsconfirmation{RESERVED} = 1;
$needsconfirmation{'resfirstname'} = $resborrower->{'firstname'};
$needsconfirmation{'ressurname'} = $resborrower->{'surname'};
$needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'};
$needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'};
$needsconfirmation{'resbranchname'} = $branchname;
$needsconfirmation{'resreservedate'} = format_date($res->{'reservedate'});
}
}
}
}
## CHECK AGE RESTRICTION
# get $marker from preferences. Could be something like "FSK|PEGI|Alter|Age:"
my $markers = C4::Context->preference('AgeRestrictionMarker');
my $bibvalues = $biblioitem->{'agerestriction'};
my $restriction_age = GetAgeRestriction( $bibvalues );
if ( $restriction_age > 0 ) {
if ( $borrower->{'dateofbirth'} ) {
my @alloweddate = split /-/, $borrower->{'dateofbirth'};
$alloweddate[0] += $restriction_age;
#Prevent runime eror on leap year (invalid date)
if ( ( $alloweddate[1] == 2 ) && ( $alloweddate[2] == 29 ) ) {
$alloweddate[2] = 28;
}
if ( Date_to_Days(Today) < Date_to_Days(@alloweddate) - 1 ) {
if ( C4::Context->preference('AgeRestrictionOverride') ) {
$needsconfirmation{AGE_RESTRICTION} = "$bibvalues";
}
else {
$issuingimpossible{AGE_RESTRICTION} = "$bibvalues";
}
}
}
}
## check for high holds decreasing loan period
my $decrease_loan = C4::Context->preference('decreaseLoanHighHolds');
if ( $decrease_loan && $decrease_loan == 1 ) {
my ( $reserved, $num, $duration, $returndate ) =
checkHighHolds( $item, $borrower );
if ( $num >= C4::Context->preference('decreaseLoanHighHoldsValue') ) {
$needsconfirmation{HIGHHOLDS} = {
num_holds => $num,
duration => $duration,
returndate => output_pref($returndate),
};
}
}
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->{biblionumber};
require C4::Serials;
my $is_a_subscription = C4::Serials::CountSubscriptionFromBiblionumber($biblionumber);
unless ($is_a_subscription) {
my $issues = GetIssues( {
borrowernumber => $borrower->{borrowernumber},
biblionumber => $biblionumber,
} );
my @issues = $issues ? @$issues : ();
# 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 (scalar @issues > 0) {
$needsconfirmation{BIBLIO_ALREADY_ISSUED} = 1;
}
}
}
return ( \%issuingimpossible, \%needsconfirmation, \%alerts );
}
=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 from GetItem
=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 $biblio = GetBiblioFromItemNumber( $item->{itemnumber} );
my $branch = _GetCircControlBranch( $item, $borrower );
my $dbh = C4::Context->dbh;
my $sth = $dbh->prepare(
'select count(borrowernumber) as num_holds from reserves where biblionumber=?'
);
$sth->execute( $item->{'biblionumber'} );
my ($holds) = $sth->fetchrow_array;
if ($holds) {
my $issuedate = DateTime->now( time_zone => C4::Context->tz() );
my $calendar = Koha::Calendar->new( branchcode => $branch );
my $itype =
( C4::Context->preference('item-level_itypes') )
? $biblio->{'itype'}
: $biblio->{'itemtype'};
my $orig_due =
C4::Circulation::CalcDateDue( $issuedate, $itype, $branch,
$borrower );
my $reduced_datedue =
$calendar->addDate( $issuedate,
C4::Context->preference('decreaseLoanHighHoldsDuration') );
if ( DateTime->compare( $reduced_datedue, $orig_due ) == -1 ) {
return ( 1, $holds,
C4::Context->preference('decreaseLoanHighHoldsDuration'),
$reduced_datedue );
}
}
return ( 0, 0, 0, undef );
}
=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 GetMember or GetMemberDetails).
=item C<$barcode> is the barcode of the item being issued.
=item C<$datedue> is a C4::Dates 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 C4::Dates 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 $auto_renew = $params && $params->{auto_renew};
my $dbh = C4::Context->dbh;
my $barcodecheck=CheckValidBarcode($barcode);
if ($datedue && ref $datedue ne 'DateTime') {
$datedue = dt_from_string($datedue);
}
# $issuedate defaults to today.
if ( ! defined $issuedate ) {
$issuedate = DateTime->now(time_zone => C4::Context->tz());
}
else {
if ( ref $issuedate ne 'DateTime') {
$issuedate = dt_from_string($issuedate);
}
}
if ($borrower and $barcode and $barcodecheck ne '0'){#??? wtf
# find which item we issue
my $item = GetItem('', $barcode) or return; # if we don't get an Item, abort.
my $branch = _GetCircControlBranch($item,$borrower);
# get actual issuing if there is one
my $actualissue = GetItemIssue( $item->{itemnumber});
# get biblioinformation for this item
my $biblio = GetBiblioFromItemNumber($item->{itemnumber});
#
# check if we just renew the issue.
#
if ($actualissue->{borrowernumber} eq $borrower->{'borrowernumber'}) {
$datedue = AddRenewal(
$borrower->{'borrowernumber'},
$item->{'itemnumber'},
$branch,
$datedue,
$issuedate, # here interpreted as the renewal date
);
}
else {
# it's NOT a renewal
if ( $actualissue->{borrowernumber}) {
# 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
AddReturn(
$item->{'barcode'},
C4::Context->userenv->{'branch'}
);
}
MoveReserve( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $cancelreserve );
# Starting process for transfer job (checking transfert and validate it if we have one)
my ($datesent) = GetTransfers($item->{'itemnumber'});
if ($datesent) {
# updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....)
my $sth =
$dbh->prepare(
"UPDATE branchtransfers
SET datearrived = now(),
tobranch = ?,
comments = 'Forced branchtransfer'
WHERE itemnumber= ? AND datearrived IS NULL"
);
$sth->execute(C4::Context->userenv->{'branch'},$item->{'itemnumber'});
}
# If automatic renewal wasn't selected while issuing, set the value according to the issuing rule.
unless ($auto_renew) {
my $issuingrule = GetIssuingRule($borrower->{categorycode}, $item->{itype}, $branch);
$auto_renew = $issuingrule->{auto_renew};
}
# Record in the database the fact that the book was issued.
my $sth =
$dbh->prepare(
"INSERT INTO issues
(borrowernumber, itemnumber,issuedate, date_due, branchcode, onsite_checkout, auto_renew)
VALUES (?,?,?,?,?,?,?)"
);
unless ($datedue) {
my $itype = ( C4::Context->preference('item-level_itypes') ) ? $biblio->{'itype'} : $biblio->{'itemtype'};
$datedue = CalcDateDue( $issuedate, $itype, $branch, $borrower );
}
$datedue->truncate( to => 'minute');
$sth->execute(
$borrower->{'borrowernumber'}, # borrowernumber
$item->{'itemnumber'}, # itemnumber
$issuedate->strftime('%Y-%m-%d %H:%M:00'), # issuedate
$datedue->strftime('%Y-%m-%d %H:%M:00'), # date_due
C4::Context->userenv->{'branch'}, # branchcode
$onsite_checkout,
$auto_renew ? 1 : 0 # automatic renewal
);
if ( C4::Context->preference('ReturnToShelvingCart') ) { ## ReturnToShelvingCart is on, anything issued should be taken off the cart.
CartToShelf( $item->{'itemnumber'} );
}
$item->{'issues'}++;
if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) {
UpdateTotalIssues($item->{'biblionumber'}, 1);
}
## If item was lost, it has now been found, reverse any list item charges if neccessary.
if ( $item->{'itemlost'} ) {
if ( C4::Context->preference('RefundLostItemFeeOnReturn' ) ) {
_FixAccountForLostAndReturned( $item->{'itemnumber'}, undef, $item->{'barcode'} );
}
}
ModItem({ issues => $item->{'issues'},
holdingbranch => C4::Context->userenv->{'branch'},
itemlost => 0,
datelastborrowed => DateTime->now(time_zone => C4::Context->tz())->ymd(),
onloan => $datedue->ymd(),
}, $item->{'biblionumber'}, $item->{'itemnumber'});
ModDateLastSeen( $item->{'itemnumber'} );
# If it costs to borrow this book, charge it to the patron's account.
my ( $charge, $itemtype ) = GetIssuingCharges(
$item->{'itemnumber'},
$borrower->{'borrowernumber'}
);
if ( $charge > 0 ) {
AddIssuingCharge(
$item->{'itemnumber'},
$borrower->{'borrowernumber'}, $charge
);
$item->{'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->{'itemnumber'},
itemtype => $item->{'itype'},
borrowernumber => $borrower->{'borrowernumber'},
ccode => $item->{'ccode'}}
);
# Send a checkout slip.
my $circulation_alert = 'C4::ItemCirculationAlertPreference';
my %conditions = (
branchcode => $branch,
categorycode => $borrower->{categorycode},
item_type => $item->{itype},
notification => 'CHECKOUT',
);
if ($circulation_alert->is_enabled_for(\%conditions)) {
SendCirculationAlert({
type => 'CHECKOUT',
item => $item,
borrower => $borrower,
branch => $branch,
});
}
}
logaction("CIRCULATION", "ISSUE", $borrower->{'borrowernumber'}, $biblio->{'itemnumber'})
if C4::Context->preference("IssueLog");
}
return ($datedue); # not necessarily the same as when it came in!
}
=head2 GetLoanLength
my $loanlength = &GetLoanLength($borrowertype,$itemtype,branchcode)
Get loan length for an itemtype, a borrower type and a branch
=cut
sub GetLoanLength {
my ( $borrowertype, $itemtype, $branchcode ) = @_;
my $dbh = C4::Context->dbh;
my $sth = $dbh->prepare(qq{
SELECT issuelength, lengthunit, renewalperiod
FROM issuingrules
WHERE categorycode=?
AND itemtype=?
AND branchcode=?
AND issuelength IS NOT NULL
});
# try to find issuelength & return the 1st available.
# check with borrowertype, itemtype and branchcode, then without one of those parameters
$sth->execute( $borrowertype, $itemtype, $branchcode );
my $loanlength = $sth->fetchrow_hashref;
return $loanlength
if defined($loanlength) && $loanlength->{issuelength};
$sth->execute( $borrowertype, '*', $branchcode );
$loanlength = $sth->fetchrow_hashref;
return $loanlength
if defined($loanlength) && $loanlength->{issuelength};
$sth->execute( '*', $itemtype, $branchcode );
$loanlength = $sth->fetchrow_hashref;
return $loanlength
if defined($loanlength) && $loanlength->{issuelength};
$sth->execute( '*', '*', $branchcode );
$loanlength = $sth->fetchrow_hashref;
return $loanlength
if defined($loanlength) && $loanlength->{issuelength};
$sth->execute( $borrowertype, $itemtype, '*' );
$loanlength = $sth->fetchrow_hashref;
return $loanlength
if defined($loanlength) && $loanlength->{issuelength};
$sth->execute( $borrowertype, '*', '*' );
$loanlength = $sth->fetchrow_hashref;
return $loanlength
if defined($loanlength) && $loanlength->{issuelength};
$sth->execute( '*', $itemtype, '*' );
$loanlength = $sth->fetchrow_hashref;
return $loanlength
if defined($loanlength) && $loanlength->{issuelength};
$sth->execute( '*', '*', '*' );
$loanlength = $sth->fetchrow_hashref;
return $loanlength
if defined($loanlength) && $loanlength->{issuelength};
# if no rule is set => 21 days (hardcoded)
return {
issuelength => 21,
renewalperiod => 21,
lengthunit => 'days',
};
}
=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 $rule = GetIssuingRule( $borrowertype, $itemtype, $branchcode );
if ( defined( $rule ) ) {
if ( $rule->{hardduedate} ) {
return (dt_from_string($rule->{hardduedate}, 'iso'),$rule->{hardduedatecompare});
} else {
return (undef, undef);
}
}
}
=head2 GetIssuingRule
my $irule = &GetIssuingRule($borrowertype,$itemtype,branchcode)
FIXME - This is a copy-paste of GetLoanLength
as a stop-gap. Do not wish to change API for GetLoanLength
this close to release.
Get the issuing rule for an itemtype, a borrower type and a branch
Returns a hashref from the issuingrules table.
=cut
sub GetIssuingRule {
my ( $borrowertype, $itemtype, $branchcode ) = @_;
my $dbh = C4::Context->dbh;
my $sth = $dbh->prepare( "select * from issuingrules where categorycode=? and itemtype=? and branchcode=? and issuelength is not null" );
my $irule;
$sth->execute( $borrowertype, $itemtype, $branchcode );
$irule = $sth->fetchrow_hashref;
return $irule if defined($irule) ;
$sth->execute( $borrowertype, "*", $branchcode );
$irule = $sth->fetchrow_hashref;
return $irule if defined($irule) ;
$sth->execute( "*", $itemtype, $branchcode );
$irule = $sth->fetchrow_hashref;
return $irule if defined($irule) ;
$sth->execute( "*", "*", $branchcode );
$irule = $sth->fetchrow_hashref;
return $irule if defined($irule) ;
$sth->execute( $borrowertype, $itemtype, "*" );
$irule = $sth->fetchrow_hashref;
return $irule if defined($irule) ;
$sth->execute( $borrowertype, "*", "*" );
$irule = $sth->fetchrow_hashref;
return $irule if defined($irule) ;
$sth->execute( "*", $itemtype, "*" );
$irule = $sth->fetchrow_hashref;
return $irule if defined($irule) ;
$sth->execute( "*", "*", "*" );
$irule = $sth->fetchrow_hashref;
return $irule if defined($irule) ;
# if no rule matches,
return;
}
=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:
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.
This will first check for a specific branch and
category match from branch_borrower_circ_rules.
If no rule is found, it will then check default_branch_circ_rules
(same branch, default category). If no rule is found,
it will then check default_borrower_circ_rules (default
branch, same category), then failing that, default_circ_rules
(default branch, default category).
If no rule has been found in the database, it will default to
the buillt in rule:
maxissueqty - undef
C<$branchcode> and C<$categorycode> should contain the
literal branch code and patron category code, respectively - no
wildcards.
=cut
sub GetBranchBorrowerCircRule {
my $branchcode = shift;
my $categorycode = shift;
my $branch_cat_query = "SELECT maxissueqty
FROM branch_borrower_circ_rules
WHERE branchcode = ?
AND categorycode = ?";
my $dbh = C4::Context->dbh();
my $sth = $dbh->prepare($branch_cat_query);
$sth->execute($branchcode, $categorycode);
my $result;
if ($result = $sth->fetchrow_hashref()) {
return $result;
}
# try same branch, default borrower category
my $branch_query = "SELECT maxissueqty
FROM default_branch_circ_rules
WHERE branchcode = ?";
$sth = $dbh->prepare($branch_query);
$sth->execute($branchcode);
if ($result = $sth->fetchrow_hashref()) {
return $result;
}
# try default branch, same borrower category
my $category_query = "SELECT maxissueqty
FROM default_borrower_circ_rules
WHERE categorycode = ?";
$sth = $dbh->prepare($category_query);
$sth->execute($categorycode);
if ($result = $sth->fetchrow_hashref()) {
return $result;
}
# try default branch, default borrower category
my $default_query = "SELECT maxissueqty
FROM default_circ_rules";
$sth = $dbh->prepare($default_query);
$sth->execute();
if ($result = $sth->fetchrow_hashref()) {
return $result;
}
# built-in default circulation rule
return {
maxissueqty => undef,
};
}
=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
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 ) = @_;
my $dbh = C4::Context->dbh();
my $result = {};
my @attempts = (
['SELECT holdallowed, returnbranch
FROM branch_item_rules
WHERE branchcode = ?
AND itemtype = ?', $branchcode, $itemtype],
['SELECT holdallowed, returnbranch
FROM default_branch_circ_rules
WHERE branchcode = ?', $branchcode],
['SELECT holdallowed, returnbranch
FROM default_branch_item_rules
WHERE itemtype = ?', $itemtype],
['SELECT holdallowed, returnbranch
FROM default_circ_rules'],
);
foreach my $attempt (@attempts) {
my ($query, @bind_params) = @{$attempt};
my $search_result = $dbh->selectrow_hashref ( $query , {}, @bind_params )
or next;
# Since branch/category and branch/itemtype use the same per-branch
# defaults tables, we have to check that the key we want is set, not
# just that a row was returned
$result->{'holdallowed'} = $search_result->{'holdallowed'} unless ( defined $result->{'holdallowed'} );
$result->{'returnbranch'} = $search_result->{'returnbranch'} unless ( defined $result->{'returnbranch'} );
}
# built-in default circulation rule
$result->{'holdallowed'} = 2 unless ( defined $result->{'holdallowed'} );
$result->{'returnbranch'} = 'homebranch' unless ( defined $result->{'returnbranch'} );
return $result;
}
=head2 AddReturn
($doreturn, $messages, $iteminformation, $borrower) =
&AddReturn( $barcode, $branch [,$exemptfine] [,$dropbox] [,$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<$dropbox> indicates that the check-in date is assumed to be
yesterday, or the last non-holiday as defined in C4::Calendar . If
overdue charges are applied and C<$dropbox> is true, the last charge
will be removed. This assumes that the fines accrual script has run
for _today_. 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<IsPermanent>
The book's home branch is a permanent collection. If you have borrowed
this book, you are not allowed to return it. The value is the code for
the book's home branch.
=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.
=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, $dropbox, $return_date ) = @_;
if ($branch and not GetBranchDetail($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 $messages;
my $borrower;
my $biblio;
my $doreturn = 1;
my $validTransfert = 0;
my $stat_type = 'return';
# get information on item
my $itemnumber = GetItemnumberFromBarcode( $barcode );
unless ($itemnumber) {
return (0, { BadBarcode => $barcode }); # no barcode means no item or borrower. bail out.
}
my $issue = GetItemIssue($itemnumber);
# warn Dumper($iteminformation);
if ($issue and $issue->{borrowernumber}) {
$borrower = C4::Members::GetMemberDetails($issue->{borrowernumber})
or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existant borrowernumber '$issue->{borrowernumber}'\n"
. Dumper($issue) . "\n";
} else {
$messages->{'NotIssued'} = $barcode;
# 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';
}
}
my $item = GetItem($itemnumber) or die "GetItem($itemnumber) failed";
# full item data, but no borrowernumber or checkout info (no issue)
# we know GetItem should work because GetItemnumberFromBarcode worked
my $hbr = GetBranchItemRule($item->{'homebranch'}, $item->{'itype'})->{'returnbranch'} || "homebranch";
# get the proper branch to which to return the item
$hbr = $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 $borrowernumber = $borrower->{'borrowernumber'} || undef; # we don't know if we had a borrower or not
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} };
ModItem( { notforloan => $rules->{$key} }, undef, $itemnumber );
last;
}
}
}
}
# check if the book is in a permanent collection....
# FIXME -- This 'PE' attribute is largely undocumented. afaict, there's no user interface that reflects this functionality.
if ( $hbr ) {
my $branches = GetBranches(); # a potentially expensive call for a non-feature.
$branches->{$hbr}->{PE} and $messages->{'IsPermanent'} = $hbr;
}
# check if the return is allowed at this branch
my ($returnallowed, $message) = CanBookBeReturned($item, $branch);
unless ($returnallowed){
$messages->{'Wrongbranch'} = {
Wrongbranch => $branch,
Rightbranch => $message
};
$doreturn = 0;
return ( $doreturn, $messages, $issue, $borrower );
}
if ( $item->{'withdrawn'} ) { # book has been cancelled
$messages->{'withdrawn'} = 1;
$doreturn = 0 if C4::Context->preference("BlockReturnOfWithdrawnItems");
}
# case of a return of document (deal with issues and holdingbranch)
my $today = DateTime->now( time_zone => C4::Context->tz() );
if ($doreturn) {
my $datedue = $issue->{date_due};
$borrower or warn "AddReturn without current borrower";
my $circControlBranch;
if ($dropbox) {
# define circControlBranch only if dropbox mode is set
# don't allow dropbox mode to create an invalid entry in issues (issuedate > today)
# FIXME: check issuedate > returndate, factoring in holidays
#$circControlBranch = _GetCircControlBranch($item,$borrower) unless ( $item->{'issuedate'} eq C4::Dates->today('iso') );;
$circControlBranch = _GetCircControlBranch($item,$borrower);
$issue->{'overdue'} = DateTime->compare($issue->{'date_due'}, $today ) == -1 ? 1 : 0;
}
if ($borrowernumber) {
if ( ( C4::Context->preference('CalculateFinesOnReturn') && $issue->{'overdue'} ) || $return_date ) {
# we only need to calculate and change the fines if we want to do that on return
# Should be on for hourly loans
my $control = C4::Context->preference('CircControl');
my $control_branchcode =
( $control eq 'ItemHomeLibrary' ) ? $item->{homebranch}
: ( $control eq 'PatronLibrary' ) ? $borrower->{branchcode}
: $issue->{branchcode};
my $date_returned =
$return_date ? dt_from_string($return_date) : $today;
my ( $amount, $type, $unitcounttotal ) =
C4::Overdues::CalcFine( $item, $borrower->{categorycode},
$control_branchcode, $datedue, $date_returned );
$type ||= q{};
if ( C4::Context->preference('finesMode') eq 'production' ) {
if ( $amount > 0 ) {
C4::Overdues::UpdateFine( $issue->{itemnumber},
$issue->{borrowernumber},
$amount, $type, output_pref($datedue) );
}
elsif ($return_date) {
# Backdated returns may have fines that shouldn't exist,
# so in this case, we need to drop those fines to 0
C4::Overdues::UpdateFine( $issue->{itemnumber},
$issue->{borrowernumber},
0, $type, output_pref($datedue) );
}
}
}
MarkIssueReturned( $borrowernumber, $item->{'itemnumber'},
$circControlBranch, $return_date, $borrower->{'privacy'} );
# FIXME is the "= 1" right? This could be the borrower hash.
$messages->{'WasReturned'} = 1;
}
ModItem({ onloan => undef }, $issue->{'biblionumber'}, $item->{'itemnumber'});
}
# 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) {
UpdateHoldingbranch($branch, $item->{'itemnumber'});
$item->{'holdingbranch'} = $branch; # update item data holdingbranch too
}
ModDateLastSeen( $item->{'itemnumber'} );
# check if we have a transfer for this document
my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->{'itemnumber'} );
# if we have a transfer to do, we update the line of transfers with the datearrived
my $is_in_rotating_collection = C4::RotatingCollections::isItemInAnyCollection( $item->{'itemnumber'} );
if ($datesent) {
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'} );
# if we have a reservation with valid transfer, we can set it's status to 'W'
ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
C4::Reserves::ModReserveStatus($item->{'itemnumber'}, 'W');
} else {
$messages->{'WrongTransfer'} = $tobranch;
$messages->{'WrongTransferItem'} = $item->{'itemnumber'};
}
$validTransfert = 1;
} else {
ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
}
# fix up the accounts.....
if ( $item->{'itemlost'} ) {
$messages->{'WasLost'} = 1;
if ( C4::Context->preference('RefundLostItemFeeOnReturn' ) ) {
_FixAccountForLostAndReturned($item->{'itemnumber'}, $borrowernumber, $barcode); # can tolerate undef $borrowernumber
$messages->{'LostItemFeeRefunded'} = 1;
}
}
# fix up the overdues in accounts...
if ($borrowernumber) {
my $fix = _FixOverduesOnReturn($borrowernumber, $item->{itemnumber}, $exemptfine, $dropbox);
defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $item->{itemnumber}...) failed!"; # zero is OK, check defined
if ( $issue->{overdue} && $issue->{date_due} ) {
# fix fine days
my ($debardate,$reminder) = _debar_user_on_return( $borrower, $item, $issue->{date_due}, $today );
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 $borrower->{'debarred'} ) {
my $borrower_debar_dt = dt_from_string( $borrower->{debarred} );
$borrower_debar_dt->truncate(to => 'day');
my $today_dt = $today->clone()->truncate(to => 'day');
if ( DateTime->compare( $borrower_debar_dt, $today_dt ) != -1 ) {
$messages->{'PrevDebarred'} = $borrower->{'debarred'};
}
}
}
# find reserves.....
# if we don't have a reserve with the status W, we launch the Checkreserves routine
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 ($resfound) {
$resrec->{'ResFound'} = $resfound;
$messages->{'ResFound'} = $resrec;
}
# Record the fact that this book was returned.
# FIXME itemtype should record item level type, not bibliolevel type
UpdateStats({
branch => $branch,
type => $stat_type,
itemnumber => $item->{'itemnumber'},
itemtype => $biblio->{'itemtype'},
borrowernumber => $borrowernumber,
ccode => $item->{'ccode'}}
);
# Send a check-in slip. # NOTE: borrower may be undef. probably shouldn't try to send messages then.
my $circulation_alert = 'C4::ItemCirculationAlertPreference';
my %conditions = (
branchcode => $branch,
categorycode => $borrower->{categorycode},
item_type => $item->{itype},
notification => 'CHECKIN',
);
if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
SendCirculationAlert({
type => 'CHECKIN',
item => $item,
borrower => $borrower,
branch => $branch,
});
}
logaction("CIRCULATION", "RETURN", $borrowernumber, $item->{'itemnumber'})
if C4::Context->preference("ReturnLog");
# Remove any OVERDUES related debarment if the borrower has no overdues
if ( $borrowernumber
&& $borrower->{'debarred'}
&& C4::Context->preference('AutoRemoveOverduesRestrictions')
&& !HasOverdues( $borrowernumber )
&& @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
) {
DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
}
# FIXME: make this comment intelligible.
#adding message if holdingbranch is non equal a userenv branch to return the document to homebranch
#we check, if we don't have reserv or transfert for this document, if not, return it to homebranch .
if ( !$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $hbr) and not $messages->{'WrongTransfer'}){
if ( C4::Context->preference("AutomaticItemReturn" ) or
(C4::Context->preference("UseBranchTransferLimits") and
! IsBranchTransferAllowed($branch, $hbr, $item->{C4::Context->preference("BranchTransferLimitsType")} )
)) {
$debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s)", $item->{'itemnumber'},$branch, $hbr;
$debug and warn "item: " . Dumper($item);
ModItemTransfer($item->{'itemnumber'}, $branch, $hbr);
$messages->{'WasTransfered'} = 1;
} else {
$messages->{'NeedsTransfer'} = 1; # TODO: instead of 1, specify branchcode that the transfer SHOULD go to, $item->{homebranch}
}
}
return ( $doreturn, $messages, $issue, $borrower );
}
=head2 MarkIssueReturned
MarkIssueReturned($borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy);
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, or
the last non-holiday date of the branccode specified in
C<dropbox_branch> . Assumes you've already checked that
it's safe to do this, i.e. last non-holiday > issuedate.
if C<$returndate> is specified (in iso format), it is used as the date
of the return. It is ignored when a dropbox_branch is passed in.
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 needed by one
routine in C<C4::Accounts>.
=cut
sub MarkIssueReturned {
my ( $borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy ) = @_;
my $dbh = C4::Context->dbh;
my $query = 'UPDATE issues SET returndate=';
my @bind;
if ($dropbox_branch) {
my $calendar = Koha::Calendar->new( branchcode => $dropbox_branch );
my $dropboxdate = $calendar->addDate( DateTime->now( time_zone => C4::Context->tz), -1 );
$query .= ' ? ';
push @bind, $dropboxdate->strftime('%Y-%m-%d %H:%M');
} elsif ($returndate) {
$query .= ' ? ';
push @bind, $returndate;
} else {
$query .= ' now() ';
}
$query .= ' WHERE borrowernumber = ? AND itemnumber = ?';
push @bind, $borrowernumber, $itemnumber;
# FIXME transaction
my $sth_upd = $dbh->prepare($query);
$sth_upd->execute(@bind);
my $sth_copy = $dbh->prepare('INSERT INTO old_issues SELECT * FROM issues
WHERE borrowernumber = ?
AND itemnumber = ?');
$sth_copy->execute($borrowernumber, $itemnumber);
# anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
if ( $privacy == 2) {
# The default of 0 does not work due to foreign key constraints
# The anonymisation will fail quietly if AnonymousPatron is not a valid entry
# FIXME the above is unacceptable - bug 9942 relates
my $anonymouspatron = (C4::Context->preference('AnonymousPatron')) ? C4::Context->preference('AnonymousPatron') : 0;
my $sth_ano = $dbh->prepare("UPDATE old_issues SET borrowernumber=?
WHERE borrowernumber = ?
AND itemnumber = ?");
$sth_ano->execute($anonymouspatron, $borrowernumber, $itemnumber);
}
my $sth_del = $dbh->prepare("DELETE FROM issues
WHERE borrowernumber = ?
AND itemnumber = ?");
$sth_del->execute($borrowernumber, $itemnumber);
ModItem( { 'onloan' => undef }, undef, $itemnumber );
}
=head2 _debar_user_on_return
_debar_user_on_return($borrower, $item, $datedue, today);
C<$borrower> borrower hashref
C<$item> item hashref
C<$datedue> date due DateTime object
C<$today> DateTime object representing the return time
Internal function, called only by AddReturn that calculates and updates
the user fine days, and debars him if necessary.
Should only be called for overdue returns
=cut
sub _debar_user_on_return {
my ( $borrower, $item, $dt_due, $dt_today ) = @_;
my $branchcode = _GetCircControlBranch( $item, $borrower );
my $calendar = Koha::Calendar->new( branchcode => $branchcode );
# $deltadays is a DateTime::Duration object
my $deltadays = $calendar->days_between( $dt_due, $dt_today );
my $circcontrol = C4::Context->preference('CircControl');
my $issuingrule =
GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode );
my $finedays = $issuingrule->{finedays};
my $unit = $issuingrule->{lengthunit};
if ($finedays) {
# finedays is in days, so hourly loans must multiply by 24
# thus 1 hour late equals 1 day suspension * finedays rate
$finedays = $finedays * 24 if ( $unit eq 'hours' );
# grace period is measured in the same units as the loan
my $grace =
DateTime::Duration->new( $unit => $issuingrule->{firstremind} );
if ( $deltadays->subtract($grace)->is_positive() ) {
my $suspension_days = $deltadays * $finedays;
# If the max suspension days is < than the suspension days
# the suspension days is limited to this maximum period.
my $max_sd = $issuingrule->{maxsuspensiondays};
if ( defined $max_sd ) {
$max_sd = DateTime::Duration->new( days => $max_sd );
$suspension_days = $max_sd
if DateTime::Duration->compare( $max_sd, $suspension_days ) < 0;
}
my $new_debar_dt =
$dt_today->clone()->add_duration( $suspension_days );
Koha::Borrower::Debarments::AddUniqueDebarment({
borrowernumber => $borrower->{borrowernumber},
expiration => $new_debar_dt->ymd(),
type => 'SUSPENSION',
});
# if borrower was already debarred but does not get an extra debarment
if ( $borrower->{debarred} eq Koha::Borrower::Debarments::IsDebarred($borrower->{borrowernumber}) ) {
return ($borrower->{debarred},1);
}
return $new_debar_dt->ymd();
}
}
return;
}
=head2 _FixOverduesOnReturn
&_FixOverduesOnReturn($brn,$itm, $exemptfine, $dropboxmode);
C<$brn> borrowernumber
C<$itm> itemnumber
C<$exemptfine> BOOL -- remove overdue charge associated with this issue.
C<$dropboxmode> BOOL -- remove lastincrement on overdue charge associated with this issue.
Internal function, called only by AddReturn
=cut
sub _FixOverduesOnReturn {
my ($borrowernumber, $item);
unless ($borrowernumber = shift) {
warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
return;
}
unless ($item = shift) {
warn "_FixOverduesOnReturn() not supplied valid itemnumber";
return;
}
my ($exemptfine, $dropbox) = @_;
my $dbh = C4::Context->dbh;
# check for overdue fine
my $sth = $dbh->prepare(
"SELECT * FROM accountlines WHERE (borrowernumber = ?) AND (itemnumber = ?) AND (accounttype='FU' OR accounttype='O')"
);
$sth->execute( $borrowernumber, $item );
# alter fine to show that the book has been returned
my $data = $sth->fetchrow_hashref;
return 0 unless $data; # no warning, there's just nothing to fix
my $uquery;
my @bind = ($data->{'accountlines_id'});
if ($exemptfine) {
$uquery = "update accountlines set accounttype='FFOR', amountoutstanding=0";
if (C4::Context->preference("FinesLog")) {
&logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
}
} elsif ($dropbox && $data->{lastincrement}) {
my $outstanding = $data->{amountoutstanding} - $data->{lastincrement} ;
my $amt = $data->{amount} - $data->{lastincrement} ;
if (C4::Context->preference("FinesLog")) {
&logaction("FINES", 'MODIFY',$borrowernumber,"Dropbox adjustment $amt, item $item");
}
$uquery = "update accountlines set accounttype='F' ";
if($outstanding >= 0 && $amt >=0) {
$uquery .= ", amount = ? , amountoutstanding=? ";
unshift @bind, ($amt, $outstanding) ;
}
} else {
$uquery = "update accountlines set accounttype='F' ";
}
$uquery .= " where (accountlines_id = ?)";
my $usth = $dbh->prepare($uquery);
return $usth->execute(@bind);
}
=head2 _FixAccountForLostAndReturned
&_FixAccountForLostAndReturned($itemnumber, [$borrowernumber, $barcode]);
Calculates the charge for a book lost and returned.
Internal function, not exported, called only by AddReturn.
FIXME: This function reflects how inscrutable fines logic is. Fix both.
FIXME: Give a positive return value on success. It might be the $borrowernumber who received credit, or the amount forgiven.
=cut
sub _FixAccountForLostAndReturned {
my $itemnumber = shift or return;
my $borrowernumber = @_ ? shift : undef;
my $item_id = @_ ? shift : $itemnumber; # Send the barcode if you want that logged in the description
my $dbh = C4::Context->dbh;
# check for charge made for lost book
my $sth = $dbh->prepare("SELECT * FROM accountlines WHERE itemnumber = ? AND accounttype IN ('L', 'Rep', 'W') ORDER BY date DESC, accountno DESC");
$sth->execute($itemnumber);
my $data = $sth->fetchrow_hashref;
$data or return; # bail if there is nothing to do
$data->{accounttype} eq 'W' and return; # Written off
# writeoff this amount
my $offset;
my $amount = $data->{'amount'};
my $acctno = $data->{'accountno'};
my $amountleft; # Starts off undef/zero.
if ($data->{'amountoutstanding'} == $amount) {
$offset = $data->{'amount'};
$amountleft = 0; # Hey, it's zero here, too.
} else {
$offset = $amount - $data->{'amountoutstanding'}; # Um, isn't this the same as ZERO? We just tested those two things are ==
$amountleft = $data->{'amountoutstanding'} - $amount; # Um, isn't this the same as ZERO? We just tested those two things are ==
}
my $usth = $dbh->prepare("UPDATE accountlines SET accounttype = 'LR',amountoutstanding='0'
WHERE (accountlines_id = ?)");
$usth->execute($data->{'accountlines_id'}); # We might be adjusting an account for some OTHER borrowernumber now. Not the one we passed in.
#check if any credit is left if so writeoff other accounts
my $nextaccntno = getnextacctno($data->{'borrowernumber'});
$amountleft *= -1 if ($amountleft < 0);
if ($amountleft > 0) {
my $msth = $dbh->prepare("SELECT * FROM accountlines WHERE (borrowernumber = ?)
AND (amountoutstanding >0) ORDER BY date"); # might want to order by amountoustanding ASC (pay smallest first)
$msth->execute($data->{'borrowernumber'});
# offset transactions
my $newamtos;
my $accdata;
while (($accdata=$msth->fetchrow_hashref) and ($amountleft>0)){
if ($accdata->{'amountoutstanding'} < $amountleft) {
$newamtos = 0;
$amountleft -= $accdata->{'amountoutstanding'};
} else {
$newamtos = $accdata->{'amountoutstanding'} - $amountleft;
$amountleft = 0;
}
my $thisacct = $accdata->{'accountlines_id'};
# FIXME: move prepares outside while loop!
my $usth = $dbh->prepare("UPDATE accountlines SET amountoutstanding= ?
WHERE (accountlines_id = ?)");
$usth->execute($newamtos,$thisacct);
$usth = $dbh->prepare("INSERT INTO accountoffsets
(borrowernumber, accountno, offsetaccount, offsetamount)
VALUES
(?,?,?,?)");
$usth->execute($data->{'borrowernumber'},$accdata->{'accountno'},$nextaccntno,$newamtos);
}
}
$amountleft *= -1 if ($amountleft > 0);
my $desc = "Item Returned " . $item_id;
$usth = $dbh->prepare("INSERT INTO accountlines
(borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
VALUES (?,?,now(),?,?,'CR',?)");
$usth->execute($data->{'borrowernumber'},$nextaccntno,0-$amount,$desc,$amountleft);
if ($borrowernumber) {
# FIXME: same as query above. use 1 sth for both
$usth = $dbh->prepare("INSERT INTO accountoffsets
(borrowernumber, accountno, offsetaccount, offsetamount)
VALUES (?,?,?,?)");
$usth->execute($borrowernumber, $data->{'accountno'}, $nextaccntno, $offset);
}
ModItem({ paidfor => '' }, undef, $itemnumber);
return;
}
=head2 _GetCircControlBranch
my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
Internal function :
Return the library code to be used to determine which circulation
policy applies to a transaction. Looks up the CircControl and
HomeOrHoldingBranch system preferences.
C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
C<$borrower> is a hashref to borrower. Only {branchcode} is used.
=cut
sub _GetCircControlBranch {
my ($item, $borrower) = @_;
my $circcontrol = C4::Context-