Bug 23382: (follow-up) Clarify variable names
[koha.git] / C4 / Circulation.pm
blob5880abe69123af2b3a56830b15802e6833236f77
1 package C4::Circulation;
3 # Copyright 2000-2002 Katipo Communications
4 # copyright 2010 BibLibre
6 # This file is part of Koha.
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use strict;
23 #use warnings; FIXME - Bug 2505
24 use DateTime;
25 use POSIX qw( floor );
26 use Koha::DateUtils;
27 use C4::Context;
28 use C4::Stats;
29 use C4::Reserves;
30 use C4::Biblio;
31 use C4::Items;
32 use C4::Members;
33 use C4::Accounts;
34 use C4::ItemCirculationAlertPreference;
35 use C4::Message;
36 use C4::Debug;
37 use C4::Log; # logaction
38 use C4::Overdues qw(CalcFine UpdateFine get_chargeable_units);
39 use C4::RotatingCollections qw(GetCollectionItemBranches);
40 use Algorithm::CheckDigits;
42 use Data::Dumper;
43 use Koha::Account;
44 use Koha::AuthorisedValues;
45 use Koha::Biblioitems;
46 use Koha::DateUtils;
47 use Koha::Calendar;
48 use Koha::Checkouts;
49 use Koha::IssuingRules;
50 use Koha::Items;
51 use Koha::Patrons;
52 use Koha::Patron::Debarments;
53 use Koha::Database;
54 use Koha::Libraries;
55 use Koha::Account::Lines;
56 use Koha::Holds;
57 use Koha::RefundLostItemFeeRules;
58 use Koha::Account::Lines;
59 use Koha::Account::Offsets;
60 use Koha::Config::SysPrefs;
61 use Koha::Charges::Fees;
62 use Koha::Util::SystemPreferences;
63 use Carp;
64 use List::MoreUtils qw( uniq any );
65 use Scalar::Util qw( looks_like_number );
66 use Date::Calc qw(
67 Today
68 Today_and_Now
69 Add_Delta_YM
70 Add_Delta_DHMS
71 Date_to_Days
72 Day_of_Week
73 Add_Delta_Days
75 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
77 BEGIN {
78 require Exporter;
79 @ISA = qw(Exporter);
81 # FIXME subs that should probably be elsewhere
82 push @EXPORT, qw(
83 &barcodedecode
84 &LostItem
85 &ReturnLostItem
86 &GetPendingOnSiteCheckouts
89 # subs to deal with issuing a book
90 push @EXPORT, qw(
91 &CanBookBeIssued
92 &CanBookBeRenewed
93 &AddIssue
94 &AddRenewal
95 &GetRenewCount
96 &GetSoonestRenewDate
97 &GetLatestAutoRenewDate
98 &GetIssuingCharges
99 &GetBranchBorrowerCircRule
100 &GetBranchItemRule
101 &GetBiblioIssues
102 &GetOpenIssue
103 &CheckIfIssuedToPatron
104 &IsItemIssued
105 GetTopIssues
108 # subs to deal with returns
109 push @EXPORT, qw(
110 &AddReturn
111 &MarkIssueReturned
114 # subs to deal with transfers
115 push @EXPORT, qw(
116 &transferbook
117 &GetTransfers
118 &GetTransfersFromTo
119 &updateWrongTransfer
120 &DeleteTransfer
121 &IsBranchTransferAllowed
122 &CreateBranchTransferLimit
123 &DeleteBranchTransferLimits
124 &TransferSlip
127 # subs to deal with offline circulation
128 push @EXPORT, qw(
129 &GetOfflineOperations
130 &GetOfflineOperation
131 &AddOfflineOperation
132 &DeleteOfflineOperation
133 &ProcessOfflineOperation
137 =head1 NAME
139 C4::Circulation - Koha circulation module
141 =head1 SYNOPSIS
143 use C4::Circulation;
145 =head1 DESCRIPTION
147 The functions in this module deal with circulation, issues, and
148 returns, as well as general information about the library.
149 Also deals with inventory.
151 =head1 FUNCTIONS
153 =head2 barcodedecode
155 $str = &barcodedecode($barcode, [$filter]);
157 Generic filter function for barcode string.
158 Called on every circ if the System Pref itemBarcodeInputFilter is set.
159 Will do some manipulation of the barcode for systems that deliver a barcode
160 to circulation.pl that differs from the barcode stored for the item.
161 For proper functioning of this filter, calling the function on the
162 correct barcode string (items.barcode) should return an unaltered barcode.
164 The optional $filter argument is to allow for testing or explicit
165 behavior that ignores the System Pref. Valid values are the same as the
166 System Pref options.
168 =cut
170 # FIXME -- the &decode fcn below should be wrapped into this one.
171 # FIXME -- these plugins should be moved out of Circulation.pm
173 sub barcodedecode {
174 my ($barcode, $filter) = @_;
175 my $branch = C4::Context::mybranch();
176 $filter = C4::Context->preference('itemBarcodeInputFilter') unless $filter;
177 $filter or return $barcode; # ensure filter is defined, else return untouched barcode
178 if ($filter eq 'whitespace') {
179 $barcode =~ s/\s//g;
180 } elsif ($filter eq 'cuecat') {
181 chomp($barcode);
182 my @fields = split( /\./, $barcode );
183 my @results = map( decode($_), @fields[ 1 .. $#fields ] );
184 ($#results == 2) and return $results[2];
185 } elsif ($filter eq 'T-prefix') {
186 if ($barcode =~ /^[Tt](\d)/) {
187 (defined($1) and $1 eq '0') and return $barcode;
188 $barcode = substr($barcode, 2) + 0; # FIXME: probably should be substr($barcode, 1)
190 return sprintf("T%07d", $barcode);
191 # FIXME: $barcode could be "T1", causing warning: substr outside of string
192 # Why drop the nonzero digit after the T?
193 # Why pass non-digits (or empty string) to "T%07d"?
194 } elsif ($filter eq 'libsuite8') {
195 unless($barcode =~ m/^($branch)-/i){ #if barcode starts with branch code its in Koha style. Skip it.
196 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
197 $barcode =~ s/^[0]*(\d+)$/$branch-b-$1/i;
198 }else{
199 $barcode =~ s/^(\D+)[0]*(\d+)$/$branch-$1-$2/i;
202 } elsif ($filter eq 'EAN13') {
203 my $ean = CheckDigits('ean');
204 if ( $ean->is_valid($barcode) ) {
205 #$barcode = sprintf('%013d',$barcode); # this doesn't work on 32-bit systems
206 $barcode = '0' x ( 13 - length($barcode) ) . $barcode;
207 } else {
208 warn "# [$barcode] not valid EAN-13/UPC-A\n";
211 return $barcode; # return barcode, modified or not
214 =head2 decode
216 $str = &decode($chunk);
218 Decodes a segment of a string emitted by a CueCat barcode scanner and
219 returns it.
221 FIXME: Should be replaced with Barcode::Cuecat from CPAN
222 or Javascript based decoding on the client side.
224 =cut
226 sub decode {
227 my ($encoded) = @_;
228 my $seq =
229 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-';
230 my @s = map { index( $seq, $_ ); } split( //, $encoded );
231 my $l = ( $#s + 1 ) % 4;
232 if ($l) {
233 if ( $l == 1 ) {
234 # warn "Error: Cuecat decode parsing failed!";
235 return;
237 $l = 4 - $l;
238 $#s += $l;
240 my $r = '';
241 while ( $#s >= 0 ) {
242 my $n = ( ( $s[0] << 6 | $s[1] ) << 6 | $s[2] ) << 6 | $s[3];
243 $r .=
244 chr( ( $n >> 16 ) ^ 67 )
245 .chr( ( $n >> 8 & 255 ) ^ 67 )
246 .chr( ( $n & 255 ) ^ 67 );
247 @s = @s[ 4 .. $#s ];
249 $r = substr( $r, 0, length($r) - $l );
250 return $r;
253 =head2 transferbook
255 ($dotransfer, $messages, $iteminformation) = &transferbook($newbranch,
256 $barcode, $ignore_reserves);
258 Transfers an item to a new branch. If the item is currently on loan, it is automatically returned before the actual transfer.
260 C<$newbranch> is the code for the branch to which the item should be transferred.
262 C<$barcode> is the barcode of the item to be transferred.
264 If C<$ignore_reserves> is true, C<&transferbook> ignores reserves.
265 Otherwise, if an item is reserved, the transfer fails.
267 Returns three values:
269 =over
271 =item $dotransfer
273 is true if the transfer was successful.
275 =item $messages
277 is a reference-to-hash which may have any of the following keys:
279 =over
281 =item C<BadBarcode>
283 There is no item in the catalog with the given barcode. The value is C<$barcode>.
285 =item C<DestinationEqualsHolding>
287 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.
289 =item C<WasReturned>
291 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.
293 =item C<ResFound>
295 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>.
297 =item C<WasTransferred>
299 The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored.
301 =back
303 =back
305 =cut
307 sub transferbook {
308 my ( $tbr, $barcode, $ignoreRs ) = @_;
309 my $messages;
310 my $dotransfer = 1;
311 my $item = Koha::Items->find( { barcode => $barcode } );
313 # bad barcode..
314 unless ( $item ) {
315 $messages->{'BadBarcode'} = $barcode;
316 $dotransfer = 0;
317 return ( $dotransfer, $messages );
320 my $itemnumber = $item->itemnumber;
321 my $issue = GetOpenIssue($itemnumber);
322 # get branches of book...
323 my $hbr = $item->homebranch;
324 my $fbr = $item->holdingbranch;
326 # if using Branch Transfer Limits
327 if ( C4::Context->preference("UseBranchTransferLimits") == 1 ) {
328 my $code = C4::Context->preference("BranchTransferLimitsType") eq 'ccode' ? $item->ccode : $item->biblio->biblioitem->itemtype; # BranchTransferLimitsType is 'ccode' or 'itemtype'
329 if ( C4::Context->preference("item-level_itypes") && C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ) {
330 if ( ! IsBranchTransferAllowed( $tbr, $fbr, $item->itype ) ) {
331 $messages->{'NotAllowed'} = $tbr . "::" . $item->itype;
332 $dotransfer = 0;
334 } elsif ( ! IsBranchTransferAllowed( $tbr, $fbr, $code ) ) {
335 $messages->{'NotAllowed'} = $tbr . "::" . $code;
336 $dotransfer = 0;
340 # can't transfer book if is already there....
341 if ( $fbr eq $tbr ) {
342 $messages->{'DestinationEqualsHolding'} = 1;
343 $dotransfer = 0;
346 # check if it is still issued to someone, return it...
347 if ( $issue ) {
348 AddReturn( $barcode, $fbr );
349 $messages->{'WasReturned'} = $issue->borrowernumber;
352 # find reserves.....
353 # That'll save a database query.
354 my ( $resfound, $resrec, undef ) =
355 CheckReserves( $itemnumber );
356 if ( $resfound and not $ignoreRs ) {
357 $resrec->{'ResFound'} = $resfound;
359 # $messages->{'ResFound'} = $resrec;
360 $dotransfer = 1;
363 #actually do the transfer....
364 if ($dotransfer) {
365 ModItemTransfer( $itemnumber, $fbr, $tbr );
367 # don't need to update MARC anymore, we do it in batch now
368 $messages->{'WasTransfered'} = 1;
371 ModDateLastSeen( $itemnumber );
372 return ( $dotransfer, $messages );
376 sub TooMany {
377 my $borrower = shift;
378 my $item_object = shift;
379 my $params = shift;
380 my $onsite_checkout = $params->{onsite_checkout} || 0;
381 my $switch_onsite_checkout = $params->{switch_onsite_checkout} || 0;
382 my $cat_borrower = $borrower->{'categorycode'};
383 my $dbh = C4::Context->dbh;
384 my $branch;
385 # Get which branchcode we need
386 $branch = _GetCircControlBranch($item_object->unblessed,$borrower);
387 my $type = $item_object->effective_itemtype;
389 # given branch, patron category, and item type, determine
390 # applicable issuing rule
391 my $maxissueqty_rule = Koha::CirculationRules->get_effective_rule(
393 categorycode => $cat_borrower,
394 itemtype => $type,
395 branchcode => $branch,
396 rule_name => 'maxissueqty',
399 my $maxonsiteissueqty_rule = Koha::CirculationRules->get_effective_rule(
401 categorycode => $cat_borrower,
402 itemtype => $type,
403 branchcode => $branch,
404 rule_name => 'maxonsiteissueqty',
409 # if a rule is found and has a loan limit set, count
410 # how many loans the patron already has that meet that
411 # rule
412 if (defined($maxissueqty_rule) and $maxissueqty_rule->rule_value ne '') {
413 my @bind_params;
414 my $count_query = q|
415 SELECT COUNT(*) AS total, COALESCE(SUM(onsite_checkout), 0) AS onsite_checkouts
416 FROM issues
417 JOIN items USING (itemnumber)
420 my $rule_itemtype = $maxissueqty_rule->itemtype;
421 unless ($rule_itemtype) {
422 # matching rule has the default item type, so count only
423 # those existing loans that don't fall under a more
424 # specific rule
425 if (C4::Context->preference('item-level_itypes')) {
426 $count_query .= " WHERE items.itype NOT IN (
427 SELECT itemtype FROM issuingrules
428 WHERE branchcode = ?
429 AND (categorycode = ? OR categorycode = ?)
430 AND itemtype <> '*'
431 ) ";
432 } else {
433 $count_query .= " JOIN biblioitems USING (biblionumber)
434 WHERE biblioitems.itemtype NOT IN (
435 SELECT itemtype FROM issuingrules
436 WHERE branchcode = ?
437 AND (categorycode = ? OR categorycode = ?)
438 AND itemtype <> '*'
439 ) ";
441 push @bind_params, $maxissueqty_rule->branchcode;
442 push @bind_params, $maxissueqty_rule->categorycode;
443 push @bind_params, $cat_borrower;
444 } else {
445 # rule has specific item type, so count loans of that
446 # specific item type
447 if (C4::Context->preference('item-level_itypes')) {
448 $count_query .= " WHERE items.itype = ? ";
449 } else {
450 $count_query .= " JOIN biblioitems USING (biblionumber)
451 WHERE biblioitems.itemtype= ? ";
453 push @bind_params, $type;
456 $count_query .= " AND borrowernumber = ? ";
457 push @bind_params, $borrower->{'borrowernumber'};
458 my $rule_branch = $maxissueqty_rule->branchcode;
459 if ($rule_branch) {
460 if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
461 $count_query .= " AND issues.branchcode = ? ";
462 push @bind_params, $rule_branch;
463 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
464 ; # if branch is the patron's home branch, then count all loans by patron
465 } else {
466 $count_query .= " AND items.homebranch = ? ";
467 push @bind_params, $rule_branch;
471 my ( $checkout_count, $onsite_checkout_count ) = $dbh->selectrow_array( $count_query, {}, @bind_params );
473 my $max_checkouts_allowed = $maxissueqty_rule ? $maxissueqty_rule->rule_value : undef;
474 my $max_onsite_checkouts_allowed = $maxonsiteissueqty_rule ? $maxonsiteissueqty_rule->rule_value : undef;
476 if ( $onsite_checkout and defined $max_onsite_checkouts_allowed ) {
477 if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed ) {
478 return {
479 reason => 'TOO_MANY_ONSITE_CHECKOUTS',
480 count => $onsite_checkout_count,
481 max_allowed => $max_onsite_checkouts_allowed,
485 if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) {
486 my $delta = $switch_onsite_checkout ? 1 : 0;
487 if ( $checkout_count >= $max_checkouts_allowed + $delta ) {
488 return {
489 reason => 'TOO_MANY_CHECKOUTS',
490 count => $checkout_count,
491 max_allowed => $max_checkouts_allowed,
494 } elsif ( not $onsite_checkout ) {
495 if ( $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed ) {
496 return {
497 reason => 'TOO_MANY_CHECKOUTS',
498 count => $checkout_count - $onsite_checkout_count,
499 max_allowed => $max_checkouts_allowed,
505 # Now count total loans against the limit for the branch
506 my $branch_borrower_circ_rule = GetBranchBorrowerCircRule($branch, $cat_borrower);
507 if (defined($branch_borrower_circ_rule->{patron_maxissueqty}) and $branch_borrower_circ_rule->{patron_maxissueqty} ne '') {
508 my @bind_params = ();
509 my $branch_count_query = q|
510 SELECT COUNT(*) AS total, COALESCE(SUM(onsite_checkout), 0) AS onsite_checkouts
511 FROM issues
512 JOIN items USING (itemnumber)
513 WHERE borrowernumber = ?
515 push @bind_params, $borrower->{borrowernumber};
517 if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
518 $branch_count_query .= " AND issues.branchcode = ? ";
519 push @bind_params, $branch;
520 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
521 ; # if branch is the patron's home branch, then count all loans by patron
522 } else {
523 $branch_count_query .= " AND items.homebranch = ? ";
524 push @bind_params, $branch;
526 my ( $checkout_count, $onsite_checkout_count ) = $dbh->selectrow_array( $branch_count_query, {}, @bind_params );
527 my $max_checkouts_allowed = $branch_borrower_circ_rule->{patron_maxissueqty};
528 my $max_onsite_checkouts_allowed = $branch_borrower_circ_rule->{patron_maxonsiteissueqty};
530 if ( $onsite_checkout and $max_onsite_checkouts_allowed ne '' ) {
531 if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed ) {
532 return {
533 reason => 'TOO_MANY_ONSITE_CHECKOUTS',
534 count => $onsite_checkout_count,
535 max_allowed => $max_onsite_checkouts_allowed,
539 if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) {
540 my $delta = $switch_onsite_checkout ? 1 : 0;
541 if ( $checkout_count >= $max_checkouts_allowed + $delta ) {
542 return {
543 reason => 'TOO_MANY_CHECKOUTS',
544 count => $checkout_count,
545 max_allowed => $max_checkouts_allowed,
548 } elsif ( not $onsite_checkout ) {
549 if ( $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed ) {
550 return {
551 reason => 'TOO_MANY_CHECKOUTS',
552 count => $checkout_count - $onsite_checkout_count,
553 max_allowed => $max_checkouts_allowed,
559 if ( not defined( $maxissueqty_rule ) and not defined($branch_borrower_circ_rule->{patron_maxissueqty}) ) {
560 return { reason => 'NO_RULE_DEFINED', max_allowed => 0 };
563 # OK, the patron can issue !!!
564 return;
567 =head2 CanBookBeIssued
569 ( $issuingimpossible, $needsconfirmation, [ $alerts ] ) = CanBookBeIssued( $patron,
570 $barcode, $duedate, $inprocess, $ignore_reserves, $params );
572 Check if a book can be issued.
574 C<$issuingimpossible> and C<$needsconfirmation> are hashrefs.
576 IMPORTANT: The assumption by users of this routine is that causes blocking
577 the issue are keyed by uppercase labels and other returned
578 data is keyed in lower case!
580 =over 4
582 =item C<$patron> is a Koha::Patron
584 =item C<$barcode> is the bar code of the book being issued.
586 =item C<$duedates> is a DateTime object.
588 =item C<$inprocess> boolean switch
590 =item C<$ignore_reserves> boolean switch
592 =item C<$params> Hashref of additional parameters
594 Available keys:
595 override_high_holds - Ignore high holds
596 onsite_checkout - Checkout is an onsite checkout that will not leave the library
598 =back
600 Returns :
602 =over 4
604 =item C<$issuingimpossible> a reference to a hash. It contains reasons why issuing is impossible.
605 Possible values are :
607 =back
609 =head3 INVALID_DATE
611 sticky due date is invalid
613 =head3 GNA
615 borrower gone with no address
617 =head3 CARD_LOST
619 borrower declared it's card lost
621 =head3 DEBARRED
623 borrower debarred
625 =head3 UNKNOWN_BARCODE
627 barcode unknown
629 =head3 NOT_FOR_LOAN
631 item is not for loan
633 =head3 WTHDRAWN
635 item withdrawn.
637 =head3 RESTRICTED
639 item is restricted (set by ??)
641 C<$needsconfirmation> a reference to a hash. It contains reasons why the loan
642 could be prevented, but ones that can be overriden by the operator.
644 Possible values are :
646 =head3 DEBT
648 borrower has debts.
650 =head3 RENEW_ISSUE
652 renewing, not issuing
654 =head3 ISSUED_TO_ANOTHER
656 issued to someone else.
658 =head3 RESERVED
660 reserved for someone else.
662 =head3 INVALID_DATE
664 sticky due date is invalid or due date in the past
666 =head3 TOO_MANY
668 if the borrower borrows to much things
670 =cut
672 sub CanBookBeIssued {
673 my ( $patron, $barcode, $duedate, $inprocess, $ignore_reserves, $params ) = @_;
674 my %needsconfirmation; # filled with problems that needs confirmations
675 my %issuingimpossible; # filled with problems that causes the issue to be IMPOSSIBLE
676 my %alerts; # filled with messages that shouldn't stop issuing, but the librarian should be aware of.
677 my %messages; # filled with information messages that should be displayed.
679 my $onsite_checkout = $params->{onsite_checkout} || 0;
680 my $override_high_holds = $params->{override_high_holds} || 0;
682 my $item_object = Koha::Items->find({barcode => $barcode });
684 # MANDATORY CHECKS - unless item exists, nothing else matters
685 unless ( $item_object ) {
686 $issuingimpossible{UNKNOWN_BARCODE} = 1;
688 return ( \%issuingimpossible, \%needsconfirmation ) if %issuingimpossible;
690 my $item_unblessed = $item_object->unblessed; # Transition...
691 my $issue = $item_object->checkout;
692 my $biblio = $item_object->biblio;
694 my $biblioitem = $biblio->biblioitem;
695 my $effective_itemtype = $item_object->effective_itemtype;
696 my $dbh = C4::Context->dbh;
697 my $patron_unblessed = $patron->unblessed;
699 my $circ_library = Koha::Libraries->find( _GetCircControlBranch($item_unblessed, $patron_unblessed) );
701 # DUE DATE is OK ? -- should already have checked.
703 if ($duedate && ref $duedate ne 'DateTime') {
704 $duedate = dt_from_string($duedate);
706 my $now = DateTime->now( time_zone => C4::Context->tz() );
707 unless ( $duedate ) {
708 my $issuedate = $now->clone();
710 $duedate = CalcDateDue( $issuedate, $effective_itemtype, $circ_library->branchcode, $patron_unblessed );
712 # Offline circ calls AddIssue directly, doesn't run through here
713 # So issuingimpossible should be ok.
716 my $fees = Koha::Charges::Fees->new(
718 patron => $patron,
719 library => $circ_library,
720 item => $item_object,
721 to_date => $duedate,
725 if ($duedate) {
726 my $today = $now->clone();
727 $today->truncate( to => 'minute');
728 if (DateTime->compare($duedate,$today) == -1 ) { # duedate cannot be before now
729 $needsconfirmation{INVALID_DATE} = output_pref($duedate);
731 } else {
732 $issuingimpossible{INVALID_DATE} = output_pref($duedate);
736 # BORROWER STATUS
738 if ( $patron->category->category_type eq 'X' && ( $item_object->barcode )) {
739 # stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1 .
740 &UpdateStats({
741 branch => C4::Context->userenv->{'branch'},
742 type => 'localuse',
743 itemnumber => $item_object->itemnumber,
744 itemtype => $effective_itemtype,
745 borrowernumber => $patron->borrowernumber,
746 ccode => $item_object->ccode}
748 ModDateLastSeen( $item_object->itemnumber ); # FIXME Move to Koha::Item
749 return( { STATS => 1 }, {});
752 if ( $patron->gonenoaddress == 1 ) {
753 $issuingimpossible{GNA} = 1;
756 if ( $patron->lost == 1 ) {
757 $issuingimpossible{CARD_LOST} = 1;
759 if ( $patron->is_debarred ) {
760 $issuingimpossible{DEBARRED} = 1;
763 if ( $patron->is_expired ) {
764 $issuingimpossible{EXPIRED} = 1;
768 # BORROWER STATUS
771 # DEBTS
772 my $account = $patron->account;
773 my $balance = $account->balance;
774 my $non_issues_charges = $account->non_issues_charges;
775 my $other_charges = $balance - $non_issues_charges;
777 my $amountlimit = C4::Context->preference("noissuescharge");
778 my $allowfineoverride = C4::Context->preference("AllowFineOverride");
779 my $allfinesneedoverride = C4::Context->preference("AllFinesNeedOverride");
781 # Check the debt of this patrons guarantees
782 my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
783 $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
784 if ( defined $no_issues_charge_guarantees ) {
785 my @guarantees = map { $_->guarantee } $patron->guarantee_relationships();
786 my $guarantees_non_issues_charges;
787 foreach my $g ( @guarantees ) {
788 $guarantees_non_issues_charges += $g->account->non_issues_charges;
791 if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && !$allowfineoverride) {
792 $issuingimpossible{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
793 } elsif ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && $allowfineoverride) {
794 $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
795 } elsif ( $allfinesneedoverride && $guarantees_non_issues_charges > 0 && $guarantees_non_issues_charges <= $no_issues_charge_guarantees && !$inprocess ) {
796 $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
800 if ( C4::Context->preference("IssuingInProcess") ) {
801 if ( $non_issues_charges > $amountlimit && !$inprocess && !$allowfineoverride) {
802 $issuingimpossible{DEBT} = $non_issues_charges;
803 } elsif ( $non_issues_charges > $amountlimit && !$inprocess && $allowfineoverride) {
804 $needsconfirmation{DEBT} = $non_issues_charges;
805 } elsif ( $allfinesneedoverride && $non_issues_charges > 0 && $non_issues_charges <= $amountlimit && !$inprocess ) {
806 $needsconfirmation{DEBT} = $non_issues_charges;
809 else {
810 if ( $non_issues_charges > $amountlimit && $allowfineoverride ) {
811 $needsconfirmation{DEBT} = $non_issues_charges;
812 } elsif ( $non_issues_charges > $amountlimit && !$allowfineoverride) {
813 $issuingimpossible{DEBT} = $non_issues_charges;
814 } elsif ( $non_issues_charges > 0 && $allfinesneedoverride ) {
815 $needsconfirmation{DEBT} = $non_issues_charges;
819 if ($balance > 0 && $other_charges > 0) {
820 $alerts{OTHER_CHARGES} = sprintf( "%.2f", $other_charges );
823 $patron = Koha::Patrons->find( $patron->borrowernumber ); # FIXME Refetch just in case, to avoid regressions. But must not be needed
824 $patron_unblessed = $patron->unblessed;
826 if ( my $debarred_date = $patron->is_debarred ) {
827 # patron has accrued fine days or has a restriction. $count is a date
828 if ($debarred_date eq '9999-12-31') {
829 $issuingimpossible{USERBLOCKEDNOENDDATE} = $debarred_date;
831 else {
832 $issuingimpossible{USERBLOCKEDWITHENDDATE} = $debarred_date;
834 } elsif ( my $num_overdues = $patron->has_overdues ) {
835 ## patron has outstanding overdue loans
836 if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){
837 $issuingimpossible{USERBLOCKEDOVERDUE} = $num_overdues;
839 elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){
840 $needsconfirmation{USERBLOCKEDOVERDUE} = $num_overdues;
845 # CHECK IF BOOK ALREADY ISSUED TO THIS BORROWER
847 if ( $issue && $issue->borrowernumber eq $patron->borrowernumber ){
849 # Already issued to current borrower.
850 # If it is an on-site checkout if it can be switched to a normal checkout
851 # or ask whether the loan should be renewed
853 if ( $issue->onsite_checkout
854 and C4::Context->preference('SwitchOnSiteCheckouts') ) {
855 $messages{ONSITE_CHECKOUT_WILL_BE_SWITCHED} = 1;
856 } else {
857 my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed(
858 $patron->borrowernumber,
859 $item_object->itemnumber,
861 if ( $CanBookBeRenewed == 0 ) { # no more renewals allowed
862 if ( $renewerror eq 'onsite_checkout' ) {
863 $issuingimpossible{NO_RENEWAL_FOR_ONSITE_CHECKOUTS} = 1;
865 else {
866 $issuingimpossible{NO_MORE_RENEWALS} = 1;
869 else {
870 $needsconfirmation{RENEW_ISSUE} = 1;
874 elsif ( $issue ) {
876 # issued to someone else
878 my $patron = Koha::Patrons->find( $issue->borrowernumber );
880 my ( $can_be_returned, $message ) = CanBookBeReturned( $item_unblessed, C4::Context->userenv->{branch} );
882 unless ( $can_be_returned ) {
883 $issuingimpossible{RETURN_IMPOSSIBLE} = 1;
884 $issuingimpossible{branch_to_return} = $message;
885 } else {
886 if ( C4::Context->preference('AutoReturnCheckedOutItems') ) {
887 $alerts{RETURNED_FROM_ANOTHER} = { patron => $patron };
888 } else {
889 $needsconfirmation{ISSUED_TO_ANOTHER} = 1;
890 $needsconfirmation{issued_firstname} = $patron->firstname;
891 $needsconfirmation{issued_surname} = $patron->surname;
892 $needsconfirmation{issued_cardnumber} = $patron->cardnumber;
893 $needsconfirmation{issued_borrowernumber} = $patron->borrowernumber;
898 # JB34 CHECKS IF BORROWERS DON'T HAVE ISSUE TOO MANY BOOKS
900 my $switch_onsite_checkout = (
901 C4::Context->preference('SwitchOnSiteCheckouts')
902 and $issue
903 and $issue->onsite_checkout
904 and $issue->borrowernumber == $patron->borrowernumber ? 1 : 0 );
905 my $toomany = TooMany( $patron_unblessed, $item_object, { onsite_checkout => $onsite_checkout, switch_onsite_checkout => $switch_onsite_checkout, } );
906 # if TooMany max_allowed returns 0 the user doesn't have permission to check out this book
907 if ( $toomany && not exists $needsconfirmation{RENEW_ISSUE} ) {
908 if ( $toomany->{max_allowed} == 0 ) {
909 $needsconfirmation{PATRON_CANT} = 1;
911 if ( C4::Context->preference("AllowTooManyOverride") ) {
912 $needsconfirmation{TOO_MANY} = $toomany->{reason};
913 $needsconfirmation{current_loan_count} = $toomany->{count};
914 $needsconfirmation{max_loans_allowed} = $toomany->{max_allowed};
915 } else {
916 $issuingimpossible{TOO_MANY} = $toomany->{reason};
917 $issuingimpossible{current_loan_count} = $toomany->{count};
918 $issuingimpossible{max_loans_allowed} = $toomany->{max_allowed};
923 # CHECKPREVCHECKOUT: CHECK IF ITEM HAS EVER BEEN LENT TO PATRON
925 $patron = Koha::Patrons->find( $patron->borrowernumber ); # FIXME Refetch just in case, to avoid regressions. But must not be needed
926 my $wants_check = $patron->wants_check_for_previous_checkout;
927 $needsconfirmation{PREVISSUE} = 1
928 if ($wants_check and $patron->do_check_for_previous_checkout($item_unblessed));
931 # ITEM CHECKING
933 if ( $item_object->notforloan )
935 if(!C4::Context->preference("AllowNotForLoanOverride")){
936 $issuingimpossible{NOT_FOR_LOAN} = 1;
937 $issuingimpossible{item_notforloan} = $item_object->notforloan;
938 }else{
939 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
940 $needsconfirmation{item_notforloan} = $item_object->notforloan;
943 else {
944 # we have to check itemtypes.notforloan also
945 if (C4::Context->preference('item-level_itypes')){
946 # this should probably be a subroutine
947 my $sth = $dbh->prepare("SELECT notforloan FROM itemtypes WHERE itemtype = ?");
948 $sth->execute($effective_itemtype);
949 my $notforloan=$sth->fetchrow_hashref();
950 if ($notforloan->{'notforloan'}) {
951 if (!C4::Context->preference("AllowNotForLoanOverride")) {
952 $issuingimpossible{NOT_FOR_LOAN} = 1;
953 $issuingimpossible{itemtype_notforloan} = $effective_itemtype;
954 } else {
955 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
956 $needsconfirmation{itemtype_notforloan} = $effective_itemtype;
960 else {
961 my $itemtype = Koha::ItemTypes->find($biblioitem->itemtype);
962 if ( $itemtype and $itemtype->notforloan == 1){
963 if (!C4::Context->preference("AllowNotForLoanOverride")) {
964 $issuingimpossible{NOT_FOR_LOAN} = 1;
965 $issuingimpossible{itemtype_notforloan} = $effective_itemtype;
966 } else {
967 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
968 $needsconfirmation{itemtype_notforloan} = $effective_itemtype;
973 if ( $item_object->withdrawn && $item_object->withdrawn > 0 )
975 $issuingimpossible{WTHDRAWN} = 1;
977 if ( $item_object->restricted
978 && $item_object->restricted == 1 )
980 $issuingimpossible{RESTRICTED} = 1;
982 if ( $item_object->itemlost && C4::Context->preference("IssueLostItem") ne 'nothing' ) {
983 my $av = Koha::AuthorisedValues->search({ category => 'LOST', authorised_value => $item_object->itemlost });
984 my $code = $av->count ? $av->next->lib : '';
985 $needsconfirmation{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'confirm' );
986 $alerts{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'alert' );
988 if ( C4::Context->preference("IndependentBranches") ) {
989 my $userenv = C4::Context->userenv;
990 unless ( C4::Context->IsSuperLibrarian() ) {
991 my $HomeOrHoldingBranch = C4::Context->preference("HomeOrHoldingBranch");
992 if ( $item_object->$HomeOrHoldingBranch ne $userenv->{branch} ){
993 $issuingimpossible{ITEMNOTSAMEBRANCH} = 1;
994 $issuingimpossible{'itemhomebranch'} = $item_object->$HomeOrHoldingBranch;
996 $needsconfirmation{BORRNOTSAMEBRANCH} = $patron->branchcode
997 if ( $patron->branchcode ne $userenv->{branch} );
1002 # CHECK IF THERE IS RENTAL CHARGES. RENTAL MUST BE CONFIRMED BY THE BORROWER
1004 my $rentalConfirmation = C4::Context->preference("RentalFeesCheckoutConfirmation");
1005 if ($rentalConfirmation) {
1006 my ($rentalCharge) = GetIssuingCharges( $item_object->itemnumber, $patron->borrowernumber );
1008 my $itemtype_object = Koha::ItemTypes->find( $item_object->effective_itemtype );
1009 if ($itemtype_object) {
1010 my $accumulate_charge = $fees->accumulate_rentalcharge();
1011 if ( $accumulate_charge > 0 ) {
1012 $rentalCharge += $accumulate_charge;
1016 if ( $rentalCharge > 0 ) {
1017 $needsconfirmation{RENTALCHARGE} = $rentalCharge;
1021 unless ( $ignore_reserves ) {
1022 # See if the item is on reserve.
1023 my ( $restype, $res ) = C4::Reserves::CheckReserves( $item_object->itemnumber );
1024 if ($restype) {
1025 my $resbor = $res->{'borrowernumber'};
1026 if ( $resbor ne $patron->borrowernumber ) {
1027 my $patron = Koha::Patrons->find( $resbor );
1028 if ( $restype eq "Waiting" )
1030 # The item is on reserve and waiting, but has been
1031 # reserved by some other patron.
1032 $needsconfirmation{RESERVE_WAITING} = 1;
1033 $needsconfirmation{'resfirstname'} = $patron->firstname;
1034 $needsconfirmation{'ressurname'} = $patron->surname;
1035 $needsconfirmation{'rescardnumber'} = $patron->cardnumber;
1036 $needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
1037 $needsconfirmation{'resbranchcode'} = $res->{branchcode};
1038 $needsconfirmation{'reswaitingdate'} = $res->{'waitingdate'};
1040 elsif ( $restype eq "Reserved" ) {
1041 # The item is on reserve for someone else.
1042 $needsconfirmation{RESERVED} = 1;
1043 $needsconfirmation{'resfirstname'} = $patron->firstname;
1044 $needsconfirmation{'ressurname'} = $patron->surname;
1045 $needsconfirmation{'rescardnumber'} = $patron->cardnumber;
1046 $needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
1047 $needsconfirmation{'resbranchcode'} = $patron->branchcode;
1048 $needsconfirmation{'resreservedate'} = $res->{reservedate};
1054 ## CHECK AGE RESTRICTION
1055 my $agerestriction = $biblioitem->agerestriction;
1056 my ($restriction_age, $daysToAgeRestriction) = GetAgeRestriction( $agerestriction, $patron->unblessed );
1057 if ( $daysToAgeRestriction && $daysToAgeRestriction > 0 ) {
1058 if ( C4::Context->preference('AgeRestrictionOverride') ) {
1059 $needsconfirmation{AGE_RESTRICTION} = "$agerestriction";
1061 else {
1062 $issuingimpossible{AGE_RESTRICTION} = "$agerestriction";
1066 ## check for high holds decreasing loan period
1067 if ( C4::Context->preference('decreaseLoanHighHolds') ) {
1068 my $check = checkHighHolds( $item_unblessed, $patron_unblessed );
1070 if ( $check->{exceeded} ) {
1071 if ($override_high_holds) {
1072 $alerts{HIGHHOLDS} = {
1073 num_holds => $check->{outstanding},
1074 duration => $check->{duration},
1075 returndate => output_pref( { dt => dt_from_string($check->{due_date}), dateformat => 'iso', timeformat => '24hr' }),
1078 else {
1079 $needsconfirmation{HIGHHOLDS} = {
1080 num_holds => $check->{outstanding},
1081 duration => $check->{duration},
1082 returndate => output_pref( { dt => dt_from_string($check->{due_date}), dateformat => 'iso', timeformat => '24hr' }),
1088 if (
1089 !C4::Context->preference('AllowMultipleIssuesOnABiblio') &&
1090 # don't do the multiple loans per bib check if we've
1091 # already determined that we've got a loan on the same item
1092 !$issuingimpossible{NO_MORE_RENEWALS} &&
1093 !$needsconfirmation{RENEW_ISSUE}
1095 # Check if borrower has already issued an item from the same biblio
1096 # Only if it's not a subscription
1097 my $biblionumber = $item_object->biblionumber;
1098 require C4::Serials;
1099 my $is_a_subscription = C4::Serials::CountSubscriptionFromBiblionumber($biblionumber);
1100 unless ($is_a_subscription) {
1101 # FIXME Should be $patron->checkouts($args);
1102 my $checkouts = Koha::Checkouts->search(
1104 borrowernumber => $patron->borrowernumber,
1105 biblionumber => $biblionumber,
1108 join => 'item',
1111 # if we get here, we don't already have a loan on this item,
1112 # so if there are any loans on this bib, ask for confirmation
1113 if ( $checkouts->count ) {
1114 $needsconfirmation{BIBLIO_ALREADY_ISSUED} = 1;
1119 return ( \%issuingimpossible, \%needsconfirmation, \%alerts, \%messages, );
1122 =head2 CanBookBeReturned
1124 ($returnallowed, $message) = CanBookBeReturned($item, $branch)
1126 Check whether the item can be returned to the provided branch
1128 =over 4
1130 =item C<$item> is a hash of item information as returned Koha::Items->find->unblessed (Temporary, should be a Koha::Item instead)
1132 =item C<$branch> is the branchcode where the return is taking place
1134 =back
1136 Returns:
1138 =over 4
1140 =item C<$returnallowed> is 0 or 1, corresponding to whether the return is allowed (1) or not (0)
1142 =item C<$message> is the branchcode where the item SHOULD be returned, if the return is not allowed
1144 =back
1146 =cut
1148 sub CanBookBeReturned {
1149 my ($item, $branch) = @_;
1150 my $allowreturntobranch = C4::Context->preference("AllowReturnToBranch") || 'anywhere';
1152 # assume return is allowed to start
1153 my $allowed = 1;
1154 my $message;
1156 # identify all cases where return is forbidden
1157 if ($allowreturntobranch eq 'homebranch' && $branch ne $item->{'homebranch'}) {
1158 $allowed = 0;
1159 $message = $item->{'homebranch'};
1160 } elsif ($allowreturntobranch eq 'holdingbranch' && $branch ne $item->{'holdingbranch'}) {
1161 $allowed = 0;
1162 $message = $item->{'holdingbranch'};
1163 } elsif ($allowreturntobranch eq 'homeorholdingbranch' && $branch ne $item->{'homebranch'} && $branch ne $item->{'holdingbranch'}) {
1164 $allowed = 0;
1165 $message = $item->{'homebranch'}; # FIXME: choice of homebranch is arbitrary
1168 return ($allowed, $message);
1171 =head2 CheckHighHolds
1173 used when syspref decreaseLoanHighHolds is active. Returns 1 or 0 to define whether the minimum value held in
1174 decreaseLoanHighHoldsValue is exceeded, the total number of outstanding holds, the number of days the loan
1175 has been decreased to (held in syspref decreaseLoanHighHoldsValue), and the new due date
1177 =cut
1179 sub checkHighHolds {
1180 my ( $item, $borrower ) = @_;
1181 my $branchcode = _GetCircControlBranch( $item, $borrower );
1182 my $item_object = Koha::Items->find( $item->{itemnumber} );
1184 my $return_data = {
1185 exceeded => 0,
1186 outstanding => 0,
1187 duration => 0,
1188 due_date => undef,
1191 my $holds = Koha::Holds->search( { biblionumber => $item->{'biblionumber'} } );
1193 if ( $holds->count() ) {
1194 $return_data->{outstanding} = $holds->count();
1196 my $decreaseLoanHighHoldsControl = C4::Context->preference('decreaseLoanHighHoldsControl');
1197 my $decreaseLoanHighHoldsValue = C4::Context->preference('decreaseLoanHighHoldsValue');
1198 my $decreaseLoanHighHoldsIgnoreStatuses = C4::Context->preference('decreaseLoanHighHoldsIgnoreStatuses');
1200 my @decreaseLoanHighHoldsIgnoreStatuses = split( /,/, $decreaseLoanHighHoldsIgnoreStatuses );
1202 if ( $decreaseLoanHighHoldsControl eq 'static' ) {
1204 # static means just more than a given number of holds on the record
1206 # If the number of holds is less than the threshold, we can stop here
1207 if ( $holds->count() < $decreaseLoanHighHoldsValue ) {
1208 return $return_data;
1211 elsif ( $decreaseLoanHighHoldsControl eq 'dynamic' ) {
1213 # dynamic means X more than the number of holdable items on the record
1215 # let's get the items
1216 my @items = $holds->next()->biblio()->items()->as_list;
1218 # Remove any items with status defined to be ignored even if the would not make item unholdable
1219 foreach my $status (@decreaseLoanHighHoldsIgnoreStatuses) {
1220 @items = grep { !$_->$status } @items;
1223 # Remove any items that are not holdable for this patron
1224 @items = grep { CanItemBeReserved( $borrower->{borrowernumber}, $_->itemnumber )->{status} eq 'OK' } @items;
1226 my $items_count = scalar @items;
1228 my $threshold = $items_count + $decreaseLoanHighHoldsValue;
1230 # If the number of holds is less than the count of items we have
1231 # plus the number of holds allowed above that count, we can stop here
1232 if ( $holds->count() <= $threshold ) {
1233 return $return_data;
1237 my $issuedate = DateTime->now( time_zone => C4::Context->tz() );
1239 my $calendar = Koha::Calendar->new( branchcode => $branchcode );
1241 my $itype = $item_object->effective_itemtype;
1242 my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branchcode, $borrower );
1244 my $decreaseLoanHighHoldsDuration = C4::Context->preference('decreaseLoanHighHoldsDuration');
1246 my $reduced_datedue = $calendar->addDate( $issuedate, $decreaseLoanHighHoldsDuration );
1247 $reduced_datedue->set_hour($orig_due->hour);
1248 $reduced_datedue->set_minute($orig_due->minute);
1249 $reduced_datedue->truncate( to => 'minute' );
1251 if ( DateTime->compare( $reduced_datedue, $orig_due ) == -1 ) {
1252 $return_data->{exceeded} = 1;
1253 $return_data->{duration} = $decreaseLoanHighHoldsDuration;
1254 $return_data->{due_date} = $reduced_datedue;
1258 return $return_data;
1261 =head2 AddIssue
1263 &AddIssue($borrower, $barcode, [$datedue], [$cancelreserve], [$issuedate])
1265 Issue a book. Does no check, they are done in CanBookBeIssued. If we reach this sub, it means the user confirmed if needed.
1267 =over 4
1269 =item C<$borrower> is a hash with borrower informations (from Koha::Patron->unblessed).
1271 =item C<$barcode> is the barcode of the item being issued.
1273 =item C<$datedue> is a DateTime object for the max date of return, i.e. the date due (optional).
1274 Calculated if empty.
1276 =item C<$cancelreserve> is 1 to override and cancel any pending reserves for the item (optional).
1278 =item C<$issuedate> is the date to issue the item in iso (YYYY-MM-DD) format (optional).
1279 Defaults to today. Unlike C<$datedue>, NOT a DateTime object, unfortunately.
1281 AddIssue does the following things :
1283 - step 01: check that there is a borrowernumber & a barcode provided
1284 - check for RENEWAL (book issued & being issued to the same patron)
1285 - renewal YES = Calculate Charge & renew
1286 - renewal NO =
1287 * BOOK ACTUALLY ISSUED ? do a return if book is actually issued (but to someone else)
1288 * RESERVE PLACED ?
1289 - fill reserve if reserve to this patron
1290 - cancel reserve or not, otherwise
1291 * TRANSFERT PENDING ?
1292 - complete the transfert
1293 * ISSUE THE BOOK
1295 =back
1297 =cut
1299 sub AddIssue {
1300 my ( $borrower, $barcode, $datedue, $cancelreserve, $issuedate, $sipmode, $params ) = @_;
1302 my $onsite_checkout = $params && $params->{onsite_checkout} ? 1 : 0;
1303 my $switch_onsite_checkout = $params && $params->{switch_onsite_checkout};
1304 my $auto_renew = $params && $params->{auto_renew};
1305 my $dbh = C4::Context->dbh;
1306 my $barcodecheck = CheckValidBarcode($barcode);
1308 my $issue;
1310 if ( $datedue && ref $datedue ne 'DateTime' ) {
1311 $datedue = dt_from_string($datedue);
1314 # $issuedate defaults to today.
1315 if ( !defined $issuedate ) {
1316 $issuedate = DateTime->now( time_zone => C4::Context->tz() );
1318 else {
1319 if ( ref $issuedate ne 'DateTime' ) {
1320 $issuedate = dt_from_string($issuedate);
1325 # Stop here if the patron or barcode doesn't exist
1326 if ( $borrower && $barcode && $barcodecheck ) {
1327 # find which item we issue
1328 my $item_object = Koha::Items->find({ barcode => $barcode })
1329 or return; # if we don't get an Item, abort.
1330 my $item_unblessed = $item_object->unblessed;
1332 my $branchcode = _GetCircControlBranch( $item_unblessed, $borrower );
1334 # get actual issuing if there is one
1335 my $actualissue = $item_object->checkout;
1337 # check if we just renew the issue.
1338 if ( $actualissue and $actualissue->borrowernumber eq $borrower->{'borrowernumber'}
1339 and not $switch_onsite_checkout ) {
1340 $datedue = AddRenewal(
1341 $borrower->{'borrowernumber'},
1342 $item_object->itemnumber,
1343 $branchcode,
1344 $datedue,
1345 $issuedate, # here interpreted as the renewal date
1348 else {
1349 unless ($datedue) {
1350 my $itype = $item_object->effective_itemtype;
1351 $datedue = CalcDateDue( $issuedate, $itype, $branchcode, $borrower );
1354 $datedue->truncate( to => 'minute' );
1356 my $patron = Koha::Patrons->find( $borrower );
1357 my $library = Koha::Libraries->find( $branchcode );
1358 my $fees = Koha::Charges::Fees->new(
1360 patron => $patron,
1361 library => $library,
1362 item => $item_object,
1363 to_date => $datedue,
1367 # it's NOT a renewal
1368 if ( $actualissue and not $switch_onsite_checkout ) {
1369 # This book is currently on loan, but not to the person
1370 # who wants to borrow it now. mark it returned before issuing to the new borrower
1371 my ( $allowed, $message ) = CanBookBeReturned( $item_unblessed, C4::Context->userenv->{branch} );
1372 return unless $allowed;
1373 AddReturn( $item_object->barcode, C4::Context->userenv->{'branch'} );
1376 C4::Reserves::MoveReserve( $item_object->itemnumber, $borrower->{'borrowernumber'}, $cancelreserve );
1378 # Starting process for transfer job (checking transfert and validate it if we have one)
1379 my ($datesent) = GetTransfers( $item_object->itemnumber );
1380 if ($datesent) {
1381 # updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....)
1382 my $sth = $dbh->prepare(
1383 "UPDATE branchtransfers
1384 SET datearrived = now(),
1385 tobranch = ?,
1386 comments = 'Forced branchtransfer'
1387 WHERE itemnumber= ? AND datearrived IS NULL"
1389 $sth->execute( C4::Context->userenv->{'branch'},
1390 $item_object->itemnumber );
1393 # If automatic renewal wasn't selected while issuing, set the value according to the issuing rule.
1394 unless ($auto_renew) {
1395 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
1396 { categorycode => $borrower->{categorycode},
1397 itemtype => $item_object->effective_itemtype,
1398 branchcode => $branchcode
1402 $auto_renew = $issuing_rule->auto_renew if $issuing_rule;
1405 # Record in the database the fact that the book was issued.
1406 unless ($datedue) {
1407 my $itype = $item_object->effective_itemtype;
1408 $datedue = CalcDateDue( $issuedate, $itype, $branchcode, $borrower );
1411 $datedue->truncate( to => 'minute' );
1413 my $issue_attributes = {
1414 borrowernumber => $borrower->{'borrowernumber'},
1415 issuedate => $issuedate->strftime('%Y-%m-%d %H:%M:%S'),
1416 date_due => $datedue->strftime('%Y-%m-%d %H:%M:%S'),
1417 branchcode => C4::Context->userenv->{'branch'},
1418 onsite_checkout => $onsite_checkout,
1419 auto_renew => $auto_renew ? 1 : 0,
1422 $issue = Koha::Checkouts->find( { itemnumber => $item_object->itemnumber } );
1423 if ($issue) {
1424 $issue->set($issue_attributes)->store;
1426 else {
1427 $issue = Koha::Checkout->new(
1429 itemnumber => $item_object->itemnumber,
1430 %$issue_attributes,
1432 )->store;
1434 if ( $item_object->location eq 'CART' && $item_object->permanent_location ne 'CART' ) {
1435 ## Item was moved to cart via UpdateItemLocationOnCheckin, anything issued should be taken off the cart.
1436 CartToShelf( $item_object->itemnumber );
1439 if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) {
1440 UpdateTotalIssues( $item_object->biblionumber, 1 );
1443 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1444 if ( $item_object->itemlost ) {
1445 if (
1446 Koha::RefundLostItemFeeRules->should_refund(
1448 current_branch => C4::Context->userenv->{branch},
1449 item_home_branch => $item_object->homebranch,
1450 item_holding_branch => $item_object->holdingbranch,
1455 _FixAccountForLostAndReturned( $item_object->itemnumber, undef,
1456 $item_object->barcode );
1460 ModItem(
1462 issues => $item_object->issues + 1,
1463 holdingbranch => C4::Context->userenv->{'branch'},
1464 itemlost => 0,
1465 onloan => $datedue->ymd(),
1466 datelastborrowed => DateTime->now( time_zone => C4::Context->tz() )->ymd(),
1468 $item_object->biblionumber,
1469 $item_object->itemnumber,
1470 { log_action => 0 }
1472 ModDateLastSeen( $item_object->itemnumber );
1474 # If it costs to borrow this book, charge it to the patron's account.
1475 my ( $charge, $itemtype ) = GetIssuingCharges( $item_object->itemnumber, $borrower->{'borrowernumber'} );
1476 if ( $charge > 0 ) {
1477 AddIssuingCharge( $issue, $charge, 'RENT' );
1480 my $itemtype_object = Koha::ItemTypes->find( $item_object->effective_itemtype );
1481 if ( $itemtype_object ) {
1482 my $accumulate_charge = $fees->accumulate_rentalcharge();
1483 if ( $accumulate_charge > 0 ) {
1484 AddIssuingCharge( $issue, $accumulate_charge, 'RENT_DAILY' );
1485 $charge += $accumulate_charge;
1486 $item_unblessed->{charge} = $charge;
1490 # Record the fact that this book was issued.
1491 &UpdateStats(
1493 branch => C4::Context->userenv->{'branch'},
1494 type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
1495 amount => $charge,
1496 other => ( $sipmode ? "SIP-$sipmode" : '' ),
1497 itemnumber => $item_object->itemnumber,
1498 itemtype => $item_object->effective_itemtype,
1499 location => $item_object->location,
1500 borrowernumber => $borrower->{'borrowernumber'},
1501 ccode => $item_object->ccode,
1505 # Send a checkout slip.
1506 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1507 my %conditions = (
1508 branchcode => $branchcode,
1509 categorycode => $borrower->{categorycode},
1510 item_type => $item_object->effective_itemtype,
1511 notification => 'CHECKOUT',
1513 if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
1514 SendCirculationAlert(
1516 type => 'CHECKOUT',
1517 item => $item_object->unblessed,
1518 borrower => $borrower,
1519 branch => $branchcode,
1523 logaction(
1524 "CIRCULATION", "ISSUE",
1525 $borrower->{'borrowernumber'},
1526 $item_object->itemnumber,
1527 ) if C4::Context->preference("IssueLog");
1530 return $issue;
1533 =head2 GetLoanLength
1535 my $loanlength = &GetLoanLength($borrowertype,$itemtype,branchcode)
1537 Get loan length for an itemtype, a borrower type and a branch
1539 =cut
1541 sub GetLoanLength {
1542 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1543 my $dbh = C4::Context->dbh;
1544 my $sth = $dbh->prepare(qq{
1545 SELECT issuelength, lengthunit, renewalperiod
1546 FROM issuingrules
1547 WHERE categorycode=?
1548 AND itemtype=?
1549 AND branchcode=?
1550 AND issuelength IS NOT NULL
1553 # try to find issuelength & return the 1st available.
1554 # check with borrowertype, itemtype and branchcode, then without one of those parameters
1555 $sth->execute( $borrowertype, $itemtype, $branchcode );
1556 my $loanlength = $sth->fetchrow_hashref;
1558 return $loanlength
1559 if defined($loanlength) && defined $loanlength->{issuelength};
1561 $sth->execute( $borrowertype, '*', $branchcode );
1562 $loanlength = $sth->fetchrow_hashref;
1563 return $loanlength
1564 if defined($loanlength) && defined $loanlength->{issuelength};
1566 $sth->execute( '*', $itemtype, $branchcode );
1567 $loanlength = $sth->fetchrow_hashref;
1568 return $loanlength
1569 if defined($loanlength) && defined $loanlength->{issuelength};
1571 $sth->execute( '*', '*', $branchcode );
1572 $loanlength = $sth->fetchrow_hashref;
1573 return $loanlength
1574 if defined($loanlength) && defined $loanlength->{issuelength};
1576 $sth->execute( $borrowertype, $itemtype, '*' );
1577 $loanlength = $sth->fetchrow_hashref;
1578 return $loanlength
1579 if defined($loanlength) && defined $loanlength->{issuelength};
1581 $sth->execute( $borrowertype, '*', '*' );
1582 $loanlength = $sth->fetchrow_hashref;
1583 return $loanlength
1584 if defined($loanlength) && defined $loanlength->{issuelength};
1586 $sth->execute( '*', $itemtype, '*' );
1587 $loanlength = $sth->fetchrow_hashref;
1588 return $loanlength
1589 if defined($loanlength) && defined $loanlength->{issuelength};
1591 $sth->execute( '*', '*', '*' );
1592 $loanlength = $sth->fetchrow_hashref;
1593 return $loanlength
1594 if defined($loanlength) && defined $loanlength->{issuelength};
1596 # if no rule is set => 0 day (hardcoded)
1597 return {
1598 issuelength => 0,
1599 renewalperiod => 0,
1600 lengthunit => 'days',
1606 =head2 GetHardDueDate
1608 my ($hardduedate,$hardduedatecompare) = &GetHardDueDate($borrowertype,$itemtype,branchcode)
1610 Get the Hard Due Date and it's comparison for an itemtype, a borrower type and a branch
1612 =cut
1614 sub GetHardDueDate {
1615 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1617 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
1618 { categorycode => $borrowertype,
1619 itemtype => $itemtype,
1620 branchcode => $branchcode
1625 if ( defined( $issuing_rule ) ) {
1626 if ( $issuing_rule->hardduedate ) {
1627 return (dt_from_string($issuing_rule->hardduedate, 'iso'),$issuing_rule->hardduedatecompare);
1628 } else {
1629 return (undef, undef);
1634 =head2 GetBranchBorrowerCircRule
1636 my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
1638 Retrieves circulation rule attributes that apply to the given
1639 branch and patron category, regardless of item type.
1640 The return value is a hashref containing the following key:
1642 patron_maxissueqty - maximum number of loans that a
1643 patron of the given category can have at the given
1644 branch. If the value is undef, no limit.
1646 patron_maxonsiteissueqty - maximum of on-site checkouts that a
1647 patron of the given category can have at the given
1648 branch. If the value is undef, no limit.
1650 This will check for different branch/category combinations in the following order:
1651 branch and category
1652 branch only
1653 category only
1654 default branch and category
1656 If no rule has been found in the database, it will default to
1657 the buillt in rule:
1659 patron_maxissueqty - undef
1660 patron_maxonsiteissueqty - undef
1662 C<$branchcode> and C<$categorycode> should contain the
1663 literal branch code and patron category code, respectively - no
1664 wildcards.
1666 =cut
1668 sub GetBranchBorrowerCircRule {
1669 my ( $branchcode, $categorycode ) = @_;
1671 # Initialize default values
1672 my $rules = {
1673 patron_maxissueqty => undef,
1674 patron_maxonsiteissueqty => undef,
1677 # Search for rules!
1678 foreach my $rule_name (qw( patron_maxissueqty patron_maxonsiteissueqty )) {
1679 my $rule = Koha::CirculationRules->get_effective_rule(
1681 categorycode => $categorycode,
1682 itemtype => undef,
1683 branchcode => $branchcode,
1684 rule_name => $rule_name,
1688 $rules->{$rule_name} = $rule->rule_value if defined $rule;
1691 return $rules;
1694 =head2 GetBranchItemRule
1696 my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
1698 Retrieves circulation rule attributes that apply to the given
1699 branch and item type, regardless of patron category.
1701 The return value is a hashref containing the following keys:
1703 holdallowed => Hold policy for this branch and itemtype. Possible values:
1704 0: No holds allowed.
1705 1: Holds allowed only by patrons that have the same homebranch as the item.
1706 2: Holds allowed from any patron.
1708 returnbranch => branch to which to return item. Possible values:
1709 noreturn: do not return, let item remain where checked in (floating collections)
1710 homebranch: return to item's home branch
1711 holdingbranch: return to issuer branch
1713 This searches branchitemrules in the following order:
1715 * Same branchcode and itemtype
1716 * Same branchcode, itemtype '*'
1717 * branchcode '*', same itemtype
1718 * branchcode and itemtype '*'
1720 Neither C<$branchcode> nor C<$itemtype> should be '*'.
1722 =cut
1724 sub GetBranchItemRule {
1725 my ( $branchcode, $itemtype ) = @_;
1727 # Search for rules!
1728 my $holdallowed_rule = Koha::CirculationRules->get_effective_rule(
1730 branchcode => $branchcode,
1731 itemtype => $itemtype,
1732 rule_name => 'holdallowed',
1735 my $hold_fulfillment_policy_rule = Koha::CirculationRules->get_effective_rule(
1737 branchcode => $branchcode,
1738 itemtype => $itemtype,
1739 rule_name => 'hold_fulfillment_policy',
1742 my $returnbranch_rule = Koha::CirculationRules->get_effective_rule(
1744 branchcode => $branchcode,
1745 itemtype => $itemtype,
1746 rule_name => 'returnbranch',
1750 # built-in default circulation rule
1751 my $rules;
1752 $rules->{holdallowed} = defined $holdallowed_rule
1753 ? $holdallowed_rule->rule_value
1754 : 2;
1755 $rules->{hold_fulfillment_policy} = defined $hold_fulfillment_policy_rule
1756 ? $hold_fulfillment_policy_rule->rule_value
1757 : 'any';
1758 $rules->{returnbranch} = defined $returnbranch_rule
1759 ? $returnbranch_rule->rule_value
1760 : 'homebranch';
1762 return $rules;
1765 =head2 AddReturn
1767 ($doreturn, $messages, $iteminformation, $borrower) =
1768 &AddReturn( $barcode, $branch [,$exemptfine] [,$returndate] );
1770 Returns a book.
1772 =over 4
1774 =item C<$barcode> is the bar code of the book being returned.
1776 =item C<$branch> is the code of the branch where the book is being returned.
1778 =item C<$exemptfine> indicates that overdue charges for the item will be
1779 removed. Optional.
1781 =item C<$return_date> allows the default return date to be overridden
1782 by the given return date. Optional.
1784 =back
1786 C<&AddReturn> returns a list of four items:
1788 C<$doreturn> is true iff the return succeeded.
1790 C<$messages> is a reference-to-hash giving feedback on the operation.
1791 The keys of the hash are:
1793 =over 4
1795 =item C<BadBarcode>
1797 No item with this barcode exists. The value is C<$barcode>.
1799 =item C<NotIssued>
1801 The book is not currently on loan. The value is C<$barcode>.
1803 =item C<withdrawn>
1805 This book has been withdrawn/cancelled. The value should be ignored.
1807 =item C<Wrongbranch>
1809 This book has was returned to the wrong branch. The value is a hashref
1810 so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
1811 contain the branchcode of the incorrect and correct return library, respectively.
1813 =item C<ResFound>
1815 The item was reserved. The value is a reference-to-hash whose keys are
1816 fields from the reserves table of the Koha database, and
1817 C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
1818 either C<Waiting>, C<Reserved>, or 0.
1820 =item C<WasReturned>
1822 Value 1 if return is successful.
1824 =item C<NeedsTransfer>
1826 If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer.
1828 =back
1830 C<$iteminformation> is a reference-to-hash, giving information about the
1831 returned item from the issues table.
1833 C<$borrower> is a reference-to-hash, giving information about the
1834 patron who last borrowed the book.
1836 =cut
1838 sub AddReturn {
1839 my ( $barcode, $branch, $exemptfine, $return_date ) = @_;
1841 if ($branch and not Koha::Libraries->find($branch)) {
1842 warn "AddReturn error: branch '$branch' not found. Reverting to " . C4::Context->userenv->{'branch'};
1843 undef $branch;
1845 $branch = C4::Context->userenv->{'branch'} unless $branch; # we trust userenv to be a safe fallback/default
1846 $return_date //= dt_from_string();
1847 my $messages;
1848 my $patron;
1849 my $doreturn = 1;
1850 my $validTransfert = 0;
1851 my $stat_type = 'return';
1853 # get information on item
1854 my $item = Koha::Items->find({ barcode => $barcode });
1855 unless ($item) {
1856 return ( 0, { BadBarcode => $barcode } ); # no barcode means no item or borrower. bail out.
1859 my $itemnumber = $item->itemnumber;
1860 my $itemtype = $item->effective_itemtype;
1862 my $issue = $item->checkout;
1863 if ( $issue ) {
1864 $patron = $issue->patron
1865 or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existent borrowernumber '" . $issue->borrowernumber . "'\n"
1866 . Dumper($issue->unblessed) . "\n";
1867 } else {
1868 $messages->{'NotIssued'} = $barcode;
1869 ModItem({ onloan => undef }, $item->biblionumber, $item->itemnumber) if defined $item->onloan;
1870 # even though item is not on loan, it may still be transferred; therefore, get current branch info
1871 $doreturn = 0;
1872 # No issue, no borrowernumber. ONLY if $doreturn, *might* you have a $borrower later.
1873 # Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
1874 if (C4::Context->preference("RecordLocalUseOnReturn")) {
1875 $messages->{'LocalUse'} = 1;
1876 $stat_type = 'localuse';
1880 my $item_unblessed = $item->unblessed;
1881 # full item data, but no borrowernumber or checkout info (no issue)
1882 my $hbr = GetBranchItemRule($item->homebranch, $itemtype)->{'returnbranch'} || "homebranch";
1883 # get the proper branch to which to return the item
1884 my $returnbranch = $hbr ne 'noreturn' ? $item->$hbr : $branch;
1885 # if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
1887 my $borrowernumber = $patron ? $patron->borrowernumber : undef; # we don't know if we had a borrower or not
1888 my $patron_unblessed = $patron ? $patron->unblessed : {};
1890 my $update_loc_rules = get_yaml_pref_hash('UpdateItemLocationOnCheckin');
1891 map { $update_loc_rules->{$_} = $update_loc_rules->{$_}[0] } keys %$update_loc_rules; #We can only move to one location so we flatten the arrays
1892 if ($update_loc_rules) {
1893 if (defined $update_loc_rules->{_ALL_}) {
1894 if ($update_loc_rules->{_ALL_} eq '_PERM_') { $update_loc_rules->{_ALL_} = $item->permanent_location; }
1895 if ($update_loc_rules->{_ALL_} eq '_BLANK_') { $update_loc_rules->{_ALL_} = ''; }
1896 if ( $item->location ne $update_loc_rules->{_ALL_}) {
1897 $messages->{'ItemLocationUpdated'} = { from => $item->location, to => $update_loc_rules->{_ALL_} };
1898 ModItem( { location => $update_loc_rules->{_ALL_} }, undef, $itemnumber );
1901 else {
1902 foreach my $key ( keys %$update_loc_rules ) {
1903 if ( $update_loc_rules->{$key} eq '_PERM_' ) { $update_loc_rules->{$key} = $item->permanent_location; }
1904 if ( $update_loc_rules->{$key} eq '_BLANK_') { $update_loc_rules->{$key} = '' ;}
1905 if ( ($item->location eq $key && $item->location ne $update_loc_rules->{$key}) || ($key eq '_BLANK_' && $item->location eq '' && $update_loc_rules->{$key} ne '') ) {
1906 $messages->{'ItemLocationUpdated'} = { from => $item->location, to => $update_loc_rules->{$key} };
1907 ModItem( { location => $update_loc_rules->{$key} }, undef, $itemnumber );
1908 last;
1914 my $yaml = C4::Context->preference('UpdateNotForLoanStatusOnCheckin');
1915 if ($yaml) {
1916 $yaml = "$yaml\n\n"; # YAML is anal on ending \n. Surplus does not hurt
1917 my $rules;
1918 eval { $rules = YAML::Load($yaml); };
1919 if ($@) {
1920 warn "Unable to parse UpdateNotForLoanStatusOnCheckin syspref : $@";
1922 else {
1923 foreach my $key ( keys %$rules ) {
1924 if ( $item->notforloan eq $key ) {
1925 $messages->{'NotForLoanStatusUpdated'} = { from => $item->notforloan, to => $rules->{$key} };
1926 ModItem( { notforloan => $rules->{$key} }, undef, $itemnumber, { log_action => 0 } );
1927 last;
1933 # check if the return is allowed at this branch
1934 my ($returnallowed, $message) = CanBookBeReturned($item_unblessed, $branch);
1935 unless ($returnallowed){
1936 $messages->{'Wrongbranch'} = {
1937 Wrongbranch => $branch,
1938 Rightbranch => $message
1940 $doreturn = 0;
1941 return ( $doreturn, $messages, $issue, $patron_unblessed);
1944 if ( $item->withdrawn ) { # book has been cancelled
1945 $messages->{'withdrawn'} = 1;
1946 $doreturn = 0 if C4::Context->preference("BlockReturnOfWithdrawnItems");
1949 if ( $item->itemlost and C4::Context->preference("BlockReturnOfLostItems") ) {
1950 $doreturn = 0;
1953 # case of a return of document (deal with issues and holdingbranch)
1954 if ($doreturn) {
1955 my $is_overdue;
1956 die "The item is not issed and cannot be returned" unless $issue; # Just in case...
1957 $patron or warn "AddReturn without current borrower";
1958 $is_overdue = $issue->is_overdue( $return_date );
1960 if ($patron) {
1961 eval {
1962 MarkIssueReturned( $borrowernumber, $item->itemnumber, $return_date, $patron->privacy );
1964 unless ( $@ ) {
1965 if ( C4::Context->preference('CalculateFinesOnReturn') && $is_overdue && !$item->itemlost ) {
1966 _CalculateAndUpdateFine( { issue => $issue, item => $item_unblessed, borrower => $patron_unblessed, return_date => $return_date } );
1968 } else {
1969 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 );
1971 return ( 0, { WasReturned => 0, DataCorrupted => 1 }, $issue, $patron_unblessed );
1974 # FIXME is the "= 1" right? This could be the borrower hash.
1975 $messages->{'WasReturned'} = 1;
1979 ModItem( { onloan => undef }, $item->biblionumber, $item->itemnumber, { log_action => 0 } );
1982 # the holdingbranch is updated if the document is returned to another location.
1983 # this is always done regardless of whether the item was on loan or not
1984 my $item_holding_branch = $item->holdingbranch;
1985 if ($item->holdingbranch ne $branch) {
1986 UpdateHoldingbranch($branch, $item->itemnumber);
1987 $item_unblessed->{'holdingbranch'} = $branch; # update item data holdingbranch too # FIXME I guess this is for the _debar_user_on_return call later
1990 my $leave_item_lost = C4::Context->preference("BlockReturnOfLostItems") ? 1 : 0;
1991 ModDateLastSeen( $item->itemnumber, $leave_item_lost );
1993 # check if we have a transfer for this document
1994 my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->itemnumber );
1996 # if we have a transfer to do, we update the line of transfers with the datearrived
1997 my $is_in_rotating_collection = C4::RotatingCollections::isItemInAnyCollection( $item->itemnumber );
1998 if ($datesent) {
1999 if ( $tobranch eq $branch ) {
2000 my $sth = C4::Context->dbh->prepare(
2001 "UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL"
2003 $sth->execute( $item->itemnumber );
2004 # if we have a reservation with valid transfer, we can set it's status to 'W'
2005 C4::Reserves::ModReserveStatus($item->itemnumber, 'W');
2006 } else {
2007 $messages->{'WrongTransfer'} = $tobranch;
2008 $messages->{'WrongTransferItem'} = $item->itemnumber;
2010 $validTransfert = 1;
2013 # fix up the accounts.....
2014 if ( $item->itemlost ) {
2015 $messages->{'WasLost'} = 1;
2016 unless ( C4::Context->preference("BlockReturnOfLostItems") ) {
2017 if (
2018 Koha::RefundLostItemFeeRules->should_refund(
2020 current_branch => C4::Context->userenv->{branch},
2021 item_home_branch => $item->homebranch,
2022 item_holding_branch => $item_holding_branch
2027 _FixAccountForLostAndReturned( $item->itemnumber,
2028 $borrowernumber, $barcode );
2029 $messages->{'LostItemFeeRefunded'} = 1;
2034 # fix up the overdues in accounts...
2035 if ($borrowernumber) {
2036 my $fix = _FixOverduesOnReturn( $borrowernumber, $item->itemnumber, $exemptfine, 'RETURNED' );
2037 defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $item->itemnumber...) failed!"; # zero is OK, check defined
2039 if ( $issue and $issue->is_overdue ) {
2040 # fix fine days
2041 my ($debardate,$reminder) = _debar_user_on_return( $patron_unblessed, $item_unblessed, dt_from_string($issue->date_due), $return_date );
2042 if ($reminder){
2043 $messages->{'PrevDebarred'} = $debardate;
2044 } else {
2045 $messages->{'Debarred'} = $debardate if $debardate;
2047 # there's no overdue on the item but borrower had been previously debarred
2048 } elsif ( $issue->date_due and $patron->debarred ) {
2049 if ( $patron->debarred eq "9999-12-31") {
2050 $messages->{'ForeverDebarred'} = $patron->debarred;
2051 } else {
2052 my $borrower_debar_dt = dt_from_string( $patron->debarred );
2053 $borrower_debar_dt->truncate(to => 'day');
2054 my $today_dt = $return_date->clone()->truncate(to => 'day');
2055 if ( DateTime->compare( $borrower_debar_dt, $today_dt ) != -1 ) {
2056 $messages->{'PrevDebarred'} = $patron->debarred;
2062 # find reserves.....
2063 # if we don't have a reserve with the status W, we launch the Checkreserves routine
2064 my ($resfound, $resrec);
2065 my $lookahead= C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
2066 ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->itemnumber, undef, $lookahead ) unless ( $item->withdrawn );
2067 if ($resfound) {
2068 $resrec->{'ResFound'} = $resfound;
2069 $messages->{'ResFound'} = $resrec;
2072 # Record the fact that this book was returned.
2073 UpdateStats({
2074 branch => $branch,
2075 type => $stat_type,
2076 itemnumber => $itemnumber,
2077 itemtype => $itemtype,
2078 borrowernumber => $borrowernumber,
2079 ccode => $item->ccode,
2082 # Send a check-in slip. # NOTE: borrower may be undef. Do not try to send messages then.
2083 if ( $patron ) {
2084 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2085 my %conditions = (
2086 branchcode => $branch,
2087 categorycode => $patron->categorycode,
2088 item_type => $itemtype,
2089 notification => 'CHECKIN',
2091 if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
2092 SendCirculationAlert({
2093 type => 'CHECKIN',
2094 item => $item_unblessed,
2095 borrower => $patron->unblessed,
2096 branch => $branch,
2100 logaction("CIRCULATION", "RETURN", $borrowernumber, $item->itemnumber)
2101 if C4::Context->preference("ReturnLog");
2104 # Remove any OVERDUES related debarment if the borrower has no overdues
2105 if ( $borrowernumber
2106 && $patron->debarred
2107 && C4::Context->preference('AutoRemoveOverduesRestrictions')
2108 && !Koha::Patrons->find( $borrowernumber )->has_overdues
2109 && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2111 DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2114 # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
2115 if (!$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) and not $messages->{'WrongTransfer'}){
2116 my $BranchTransferLimitsType = C4::Context->preference("BranchTransferLimitsType");
2117 if (C4::Context->preference("AutomaticItemReturn" ) or
2118 (C4::Context->preference("UseBranchTransferLimits") and
2119 ! IsBranchTransferAllowed($branch, $returnbranch, $item->$BranchTransferLimitsType )
2120 )) {
2121 $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s)", $item->itemnumber,$branch, $returnbranch;
2122 $debug and warn "item: " . Dumper($item_unblessed);
2123 ModItemTransfer($item->itemnumber, $branch, $returnbranch);
2124 $messages->{'WasTransfered'} = 1;
2125 } else {
2126 $messages->{'NeedsTransfer'} = $returnbranch;
2130 return ( $doreturn, $messages, $issue, ( $patron ? $patron->unblessed : {} ));
2133 =head2 MarkIssueReturned
2135 MarkIssueReturned($borrowernumber, $itemnumber, $returndate, $privacy);
2137 Unconditionally marks an issue as being returned by
2138 moving the C<issues> row to C<old_issues> and
2139 setting C<returndate> to the current date.
2141 if C<$returndate> is specified (in iso format), it is used as the date
2142 of the return.
2144 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
2145 the old_issue is immediately anonymised
2147 Ideally, this function would be internal to C<C4::Circulation>,
2148 not exported, but it is currently used in misc/cronjobs/longoverdue.pl
2149 and offline_circ/process_koc.pl.
2151 =cut
2153 sub MarkIssueReturned {
2154 my ( $borrowernumber, $itemnumber, $returndate, $privacy ) = @_;
2156 # Retrieve the issue
2157 my $issue = Koha::Checkouts->find( { itemnumber => $itemnumber } ) or return;
2158 my $issue_id = $issue->issue_id;
2160 my $anonymouspatron;
2161 if ( $privacy == 2 ) {
2162 # The default of 0 will not work due to foreign key constraints
2163 # The anonymisation will fail if AnonymousPatron is not a valid entry
2164 # We need to check if the anonymous patron exist, Koha will fail loudly if it does not
2165 # Note that a warning should appear on the about page (System information tab).
2166 $anonymouspatron = C4::Context->preference('AnonymousPatron');
2167 die "Fatal error: the patron ($borrowernumber) has requested their circulation history be anonymized on check-in, but the AnonymousPatron system preference is empty or not set correctly."
2168 unless Koha::Patrons->find( $anonymouspatron );
2171 my $schema = Koha::Database->schema;
2173 # FIXME Improve the return value and handle it from callers
2174 $schema->txn_do(sub {
2176 # Update the returndate value
2177 if ( $returndate ) {
2178 $issue->returndate( $returndate )->store->discard_changes; # update and refetch
2180 else {
2181 $issue->returndate( \'NOW()' )->store->discard_changes; # update and refetch
2184 # Create the old_issues entry
2185 my $old_checkout = Koha::Old::Checkout->new($issue->unblessed)->store;
2187 # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
2188 if ( $privacy == 2) {
2189 $old_checkout->borrowernumber($anonymouspatron)->store;
2192 # And finally delete the issue
2193 $issue->delete;
2195 ModItem( { 'onloan' => undef }, undef, $itemnumber, { log_action => 0 } );
2197 if ( C4::Context->preference('StoreLastBorrower') ) {
2198 my $item = Koha::Items->find( $itemnumber );
2199 my $patron = Koha::Patrons->find( $borrowernumber );
2200 $item->last_returned_by( $patron );
2204 return $issue_id;
2207 =head2 _debar_user_on_return
2209 _debar_user_on_return($borrower, $item, $datedue, $returndate);
2211 C<$borrower> borrower hashref
2213 C<$item> item hashref
2215 C<$datedue> date due DateTime object
2217 C<$returndate> DateTime object representing the return time
2219 Internal function, called only by AddReturn that calculates and updates
2220 the user fine days, and debars them if necessary.
2222 Should only be called for overdue returns
2224 =cut
2226 sub _debar_user_on_return {
2227 my ( $borrower, $item, $dt_due, $return_date ) = @_;
2229 my $branchcode = _GetCircControlBranch( $item, $borrower );
2230 $return_date //= dt_from_string();
2232 my $circcontrol = C4::Context->preference('CircControl');
2233 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
2234 { categorycode => $borrower->{categorycode},
2235 itemtype => $item->{itype},
2236 branchcode => $branchcode
2239 my $finedays = $issuing_rule ? $issuing_rule->finedays : undef;
2240 my $unit = $issuing_rule ? $issuing_rule->lengthunit : undef;
2241 my $chargeable_units = C4::Overdues::get_chargeable_units($unit, $dt_due, $return_date, $branchcode);
2243 if ($finedays) {
2245 # finedays is in days, so hourly loans must multiply by 24
2246 # thus 1 hour late equals 1 day suspension * finedays rate
2247 $finedays = $finedays * 24 if ( $unit eq 'hours' );
2249 # grace period is measured in the same units as the loan
2250 my $grace =
2251 DateTime::Duration->new( $unit => $issuing_rule->firstremind );
2253 my $deltadays = DateTime::Duration->new(
2254 days => $chargeable_units
2256 if ( $deltadays->subtract($grace)->is_positive() ) {
2257 my $suspension_days = $deltadays * $finedays;
2259 # If the max suspension days is < than the suspension days
2260 # the suspension days is limited to this maximum period.
2261 my $max_sd = $issuing_rule->maxsuspensiondays;
2262 if ( defined $max_sd ) {
2263 $max_sd = DateTime::Duration->new( days => $max_sd );
2264 $suspension_days = $max_sd
2265 if DateTime::Duration->compare( $max_sd, $suspension_days ) < 0;
2268 my ( $has_been_extended, $is_a_reminder );
2269 if ( C4::Context->preference('CumulativeRestrictionPeriods') and $borrower->{debarred} ) {
2270 my $debarment = @{ GetDebarments( { borrowernumber => $borrower->{borrowernumber}, type => 'SUSPENSION' } ) }[0];
2271 if ( $debarment ) {
2272 $return_date = dt_from_string( $debarment->{expiration}, 'sql' );
2273 $has_been_extended = 1;
2277 if ( $issuing_rule->suspension_chargeperiod > 1 ) {
2278 # No need to / 1 and do not consider / 0
2279 $suspension_days = DateTime::Duration->new(
2280 days => floor( $suspension_days->in_units('days') / $issuing_rule->suspension_chargeperiod )
2284 my $new_debar_dt;
2285 # Use the calendar or not to calculate the debarment date
2286 if ( C4::Context->preference('finesCalendar') eq 'noFinesWhenClosed' ) {
2287 my $calendar = Koha::Calendar->new(
2288 branchcode => $branchcode,
2289 days_mode => 'Calendar'
2291 $new_debar_dt = $calendar->addDate( $return_date, $suspension_days );
2293 else {
2294 $new_debar_dt = $return_date->clone()->add_duration($suspension_days);
2297 Koha::Patron::Debarments::AddUniqueDebarment({
2298 borrowernumber => $borrower->{borrowernumber},
2299 expiration => $new_debar_dt->ymd(),
2300 type => 'SUSPENSION',
2302 # if borrower was already debarred but does not get an extra debarment
2303 my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
2304 my $new_debarment_str;
2305 if ( $borrower->{debarred} eq $patron->is_debarred ) {
2306 $is_a_reminder = 1;
2307 $new_debarment_str = $borrower->{debarred};
2308 } else {
2309 $new_debarment_str = $new_debar_dt->ymd();
2311 # FIXME Should return a DateTime object
2312 return $new_debarment_str, $is_a_reminder;
2315 return;
2318 =head2 _FixOverduesOnReturn
2320 &_FixOverduesOnReturn($borrowernumber, $itemnumber, $exemptfine, $status);
2322 C<$borrowernumber> borrowernumber
2324 C<$itemnumber> itemnumber
2326 C<$exemptfine> BOOL -- remove overdue charge associated with this issue.
2328 C<$status> ENUM -- reason for fix [ RETURNED, RENEWED, LOST, FORGIVEN ]
2330 Internal function
2332 =cut
2334 sub _FixOverduesOnReturn {
2335 my ( $borrowernumber, $item, $exemptfine, $status ) = @_;
2336 unless( $borrowernumber ) {
2337 warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
2338 return;
2340 unless( $item ) {
2341 warn "_FixOverduesOnReturn() not supplied valid itemnumber";
2342 return;
2344 unless( $status ) {
2345 warn "_FixOverduesOnReturn() not supplied valid status";
2346 return;
2349 my $schema = Koha::Database->schema;
2351 my $result = $schema->txn_do(
2352 sub {
2353 # check for overdue fine
2354 my $accountlines = Koha::Account::Lines->search(
2356 borrowernumber => $borrowernumber,
2357 itemnumber => $item,
2358 debit_type_code => 'OVERDUE',
2359 status => 'UNRETURNED'
2362 return 0 unless $accountlines->count; # no warning, there's just nothing to fix
2364 my $accountline = $accountlines->next;
2365 if ($exemptfine) {
2366 my $amountoutstanding = $accountline->amountoutstanding;
2368 my $account = Koha::Account->new({patron_id => $borrowernumber});
2369 my $credit = $account->add_credit(
2371 amount => $amountoutstanding,
2372 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
2373 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
2374 interface => C4::Context->interface,
2375 type => 'FORGIVEN',
2376 item_id => $item
2380 $credit->apply({ debits => [ $accountline ], offset_type => 'Forgiven' });
2382 $accountline->status('FORGIVEN');
2384 if (C4::Context->preference("FinesLog")) {
2385 &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
2387 } else {
2388 $accountline->status($status);
2391 return $accountline->store();
2395 return $result;
2398 =head2 _FixAccountForLostAndReturned
2400 &_FixAccountForLostAndReturned($itemnumber, [$borrowernumber, $barcode]);
2402 Finds the most recent lost item charge for this item and refunds the borrower
2403 appropriatly, taking into account any payments or writeoffs already applied
2404 against the charge.
2406 Internal function, not exported, called only by AddReturn.
2408 =cut
2410 sub _FixAccountForLostAndReturned {
2411 my $itemnumber = shift or return;
2412 my $borrowernumber = @_ ? shift : undef;
2413 my $item_id = @_ ? shift : $itemnumber; # Send the barcode if you want that logged in the description
2415 my $credit;
2417 # check for charge made for lost book
2418 my $accountlines = Koha::Account::Lines->search(
2420 itemnumber => $itemnumber,
2421 debit_type_code => 'LOST',
2422 status => [ undef, { '<>' => 'RETURNED' } ]
2425 order_by => { -desc => [ 'date', 'accountlines_id' ] }
2429 return unless $accountlines->count > 0;
2430 my $accountline = $accountlines->next;
2431 my $total_to_refund = 0;
2433 return unless $accountline->borrowernumber;
2434 my $patron = Koha::Patrons->find( $accountline->borrowernumber );
2435 return unless $patron; # Patron has been deleted, nobody to credit the return to
2437 my $account = $patron->account;
2439 # Use cases
2440 if ( $accountline->amount > $accountline->amountoutstanding ) {
2441 # some amount has been cancelled. collect the offsets that are not writeoffs
2442 # this works because the only way to subtract from this kind of a debt is
2443 # using the UI buttons 'Pay' and 'Write off'
2444 my $credits_offsets = Koha::Account::Offsets->search({
2445 debit_id => $accountline->id,
2446 credit_id => { '!=' => undef }, # it is not the debit itself
2447 type => { '!=' => 'Writeoff' },
2448 amount => { '<' => 0 } # credits are negative on the DB
2451 $total_to_refund = ( $credits_offsets->count > 0 )
2452 ? $credits_offsets->total * -1 # credits are negative on the DB
2453 : 0;
2456 my $credit_total = $accountline->amountoutstanding + $total_to_refund;
2458 if ( $credit_total > 0 ) {
2459 my $branchcode = C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef;
2460 $credit = $account->add_credit(
2461 { amount => $credit_total,
2462 description => 'Item Returned ' . $item_id,
2463 type => 'LOST_RETURN',
2464 interface => C4::Context->interface,
2465 library_id => $branchcode
2469 $credit->apply( { debits => [ $accountline ] } );
2472 # Update the account status
2473 $accountline->discard_changes->status('RETURNED');
2474 $accountline->store;
2476 if ( defined $account and C4::Context->preference('AccountAutoReconcile') ) {
2477 $account->reconcile_balance;
2480 return ($credit) ? $credit->id : undef;
2483 =head2 _GetCircControlBranch
2485 my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
2487 Internal function :
2489 Return the library code to be used to determine which circulation
2490 policy applies to a transaction. Looks up the CircControl and
2491 HomeOrHoldingBranch system preferences.
2493 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
2495 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
2497 =cut
2499 sub _GetCircControlBranch {
2500 my ($item, $borrower) = @_;
2501 my $circcontrol = C4::Context->preference('CircControl');
2502 my $branch;
2504 if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
2505 $branch= C4::Context->userenv->{'branch'};
2506 } elsif ($circcontrol eq 'PatronLibrary') {
2507 $branch=$borrower->{branchcode};
2508 } else {
2509 my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
2510 $branch = $item->{$branchfield};
2511 # default to item home branch if holdingbranch is used
2512 # and is not defined
2513 if (!defined($branch) && $branchfield eq 'holdingbranch') {
2514 $branch = $item->{homebranch};
2517 return $branch;
2520 =head2 GetOpenIssue
2522 $issue = GetOpenIssue( $itemnumber );
2524 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2526 C<$itemnumber> is the item's itemnumber
2528 Returns a hashref
2530 =cut
2532 sub GetOpenIssue {
2533 my ( $itemnumber ) = @_;
2534 return unless $itemnumber;
2535 my $dbh = C4::Context->dbh;
2536 my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2537 $sth->execute( $itemnumber );
2538 return $sth->fetchrow_hashref();
2542 =head2 GetBiblioIssues
2544 $issues = GetBiblioIssues($biblionumber);
2546 this function get all issues from a biblionumber.
2548 Return:
2549 C<$issues> is a reference to array which each value is ref-to-hash. This ref-to-hash contains all column from
2550 tables issues and the firstname,surname & cardnumber from borrowers.
2552 =cut
2554 sub GetBiblioIssues {
2555 my $biblionumber = shift;
2556 return unless $biblionumber;
2557 my $dbh = C4::Context->dbh;
2558 my $query = "
2559 SELECT issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2560 FROM issues
2561 LEFT JOIN borrowers ON borrowers.borrowernumber = issues.borrowernumber
2562 LEFT JOIN items ON issues.itemnumber = items.itemnumber
2563 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2564 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2565 WHERE biblio.biblionumber = ?
2566 UNION ALL
2567 SELECT old_issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2568 FROM old_issues
2569 LEFT JOIN borrowers ON borrowers.borrowernumber = old_issues.borrowernumber
2570 LEFT JOIN items ON old_issues.itemnumber = items.itemnumber
2571 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2572 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2573 WHERE biblio.biblionumber = ?
2574 ORDER BY timestamp
2576 my $sth = $dbh->prepare($query);
2577 $sth->execute($biblionumber, $biblionumber);
2579 my @issues;
2580 while ( my $data = $sth->fetchrow_hashref ) {
2581 push @issues, $data;
2583 return \@issues;
2586 =head2 GetUpcomingDueIssues
2588 my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2590 =cut
2592 sub GetUpcomingDueIssues {
2593 my $params = shift;
2595 $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2596 my $dbh = C4::Context->dbh;
2598 my $statement = <<END_SQL;
2599 SELECT *
2600 FROM (
2601 SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2602 FROM issues
2603 LEFT JOIN items USING (itemnumber)
2604 LEFT OUTER JOIN branches USING (branchcode)
2605 WHERE returndate is NULL
2606 ) tmp
2607 WHERE days_until_due >= 0 AND days_until_due <= ?
2608 END_SQL
2610 my @bind_parameters = ( $params->{'days_in_advance'} );
2612 my $sth = $dbh->prepare( $statement );
2613 $sth->execute( @bind_parameters );
2614 my $upcoming_dues = $sth->fetchall_arrayref({});
2616 return $upcoming_dues;
2619 =head2 CanBookBeRenewed
2621 ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber[, $override_limit]);
2623 Find out whether a borrowed item may be renewed.
2625 C<$borrowernumber> is the borrower number of the patron who currently
2626 has the item on loan.
2628 C<$itemnumber> is the number of the item to renew.
2630 C<$override_limit>, if supplied with a true value, causes
2631 the limit on the number of times that the loan can be renewed
2632 (as controlled by the item type) to be ignored. Overriding also allows
2633 to renew sooner than "No renewal before" and to manually renew loans
2634 that are automatically renewed.
2636 C<$CanBookBeRenewed> returns a true value if the item may be renewed. The
2637 item must currently be on loan to the specified borrower; renewals
2638 must be allowed for the item's type; and the borrower must not have
2639 already renewed the loan. $error will contain the reason the renewal can not proceed
2641 =cut
2643 sub CanBookBeRenewed {
2644 my ( $borrowernumber, $itemnumber, $override_limit ) = @_;
2646 my $dbh = C4::Context->dbh;
2647 my $renews = 1;
2649 my $item = Koha::Items->find($itemnumber) or return ( 0, 'no_item' );
2650 my $issue = $item->checkout or return ( 0, 'no_checkout' );
2651 return ( 0, 'onsite_checkout' ) if $issue->onsite_checkout;
2652 return ( 0, 'item_denied_renewal') if _item_denied_renewal({ item => $item });
2654 my $patron = $issue->patron or return;
2656 my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2658 # This item can fill one or more unfilled reserve, can those unfilled reserves
2659 # all be filled by other available items?
2660 if ( $resfound
2661 && C4::Context->preference('AllowRenewalIfOtherItemsAvailable') )
2663 my $schema = Koha::Database->new()->schema();
2665 my $item_holds = $schema->resultset('Reserve')->search( { itemnumber => $itemnumber, found => undef } )->count();
2666 if ($item_holds) {
2667 # There is an item level hold on this item, no other item can fill the hold
2668 $resfound = 1;
2670 else {
2672 # Get all other items that could possibly fill reserves
2673 my @itemnumbers = $schema->resultset('Item')->search(
2675 biblionumber => $resrec->{biblionumber},
2676 onloan => undef,
2677 notforloan => 0,
2678 -not => { itemnumber => $itemnumber }
2680 { columns => 'itemnumber' }
2681 )->get_column('itemnumber')->all();
2683 # Get all other reserves that could have been filled by this item
2684 my @borrowernumbers;
2685 while (1) {
2686 my ( $reserve_found, $reserve, undef ) =
2687 C4::Reserves::CheckReserves( $itemnumber, undef, undef, \@borrowernumbers );
2689 if ($reserve_found) {
2690 push( @borrowernumbers, $reserve->{borrowernumber} );
2692 else {
2693 last;
2697 # If the count of the union of the lists of reservable items for each borrower
2698 # is equal or greater than the number of borrowers, we know that all reserves
2699 # can be filled with available items. We can get the union of the sets simply
2700 # by pushing all the elements onto an array and removing the duplicates.
2701 my @reservable;
2702 my %patrons;
2703 ITEM: foreach my $itemnumber (@itemnumbers) {
2704 my $item = Koha::Items->find( $itemnumber );
2705 next if IsItemOnHoldAndFound( $itemnumber );
2706 for my $borrowernumber (@borrowernumbers) {
2707 my $patron = $patrons{$borrowernumber} //= Koha::Patrons->find( $borrowernumber );
2708 next unless IsAvailableForItemLevelRequest($item, $patron);
2709 next unless CanItemBeReserved($borrowernumber,$itemnumber);
2711 push @reservable, $itemnumber;
2712 if (@reservable >= @borrowernumbers) {
2713 $resfound = 0;
2714 last ITEM;
2716 last;
2721 return ( 0, "on_reserve" ) if $resfound; # '' when no hold was found
2723 return ( 1, undef ) if $override_limit;
2725 my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
2726 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
2727 { categorycode => $patron->categorycode,
2728 itemtype => $item->effective_itemtype,
2729 branchcode => $branchcode
2733 return ( 0, "too_many" )
2734 if not $issuing_rule or $issuing_rule->renewalsallowed <= $issue->renewals;
2736 my $overduesblockrenewing = C4::Context->preference('OverduesBlockRenewing');
2737 my $restrictionblockrenewing = C4::Context->preference('RestrictionBlockRenewing');
2738 $patron = Koha::Patrons->find($borrowernumber); # FIXME Is this really useful?
2739 my $restricted = $patron->is_debarred;
2740 my $hasoverdues = $patron->has_overdues;
2742 if ( $restricted and $restrictionblockrenewing ) {
2743 return ( 0, 'restriction');
2744 } elsif ( ($hasoverdues and $overduesblockrenewing eq 'block') || ($issue->is_overdue and $overduesblockrenewing eq 'blockitem') ) {
2745 return ( 0, 'overdue');
2748 if ( $issue->auto_renew ) {
2750 if ( $patron->category->effective_BlockExpiredPatronOpacActions and $patron->is_expired ) {
2751 return ( 0, 'auto_account_expired' );
2754 if ( defined $issuing_rule->no_auto_renewal_after
2755 and $issuing_rule->no_auto_renewal_after ne "" ) {
2756 # Get issue_date and add no_auto_renewal_after
2757 # If this is greater than today, it's too late for renewal.
2758 my $maximum_renewal_date = dt_from_string($issue->issuedate, 'sql');
2759 $maximum_renewal_date->add(
2760 $issuing_rule->lengthunit => $issuing_rule->no_auto_renewal_after
2762 my $now = dt_from_string;
2763 if ( $now >= $maximum_renewal_date ) {
2764 return ( 0, "auto_too_late" );
2767 if ( defined $issuing_rule->no_auto_renewal_after_hard_limit
2768 and $issuing_rule->no_auto_renewal_after_hard_limit ne "" ) {
2769 # If no_auto_renewal_after_hard_limit is >= today, it's also too late for renewal
2770 if ( dt_from_string >= dt_from_string( $issuing_rule->no_auto_renewal_after_hard_limit ) ) {
2771 return ( 0, "auto_too_late" );
2775 if ( C4::Context->preference('OPACFineNoRenewalsBlockAutoRenew') ) {
2776 my $fine_no_renewals = C4::Context->preference("OPACFineNoRenewals");
2777 my $amountoutstanding = $patron->account->balance;
2778 if ( $amountoutstanding and $amountoutstanding > $fine_no_renewals ) {
2779 return ( 0, "auto_too_much_oweing" );
2784 if ( defined $issuing_rule->norenewalbefore
2785 and $issuing_rule->norenewalbefore ne "" )
2788 # Calculate soonest renewal by subtracting 'No renewal before' from due date
2789 my $soonestrenewal = dt_from_string( $issue->date_due, 'sql' )->subtract(
2790 $issuing_rule->lengthunit => $issuing_rule->norenewalbefore );
2792 # Depending on syspref reset the exact time, only check the date
2793 if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
2794 and $issuing_rule->lengthunit eq 'days' )
2796 $soonestrenewal->truncate( to => 'day' );
2799 if ( $soonestrenewal > DateTime->now( time_zone => C4::Context->tz() ) )
2801 return ( 0, "auto_too_soon" ) if $issue->auto_renew;
2802 return ( 0, "too_soon" );
2804 elsif ( $issue->auto_renew ) {
2805 return ( 0, "auto_renew" );
2809 # Fallback for automatic renewals:
2810 # If norenewalbefore is undef, don't renew before due date.
2811 if ( $issue->auto_renew ) {
2812 my $now = dt_from_string;
2813 return ( 0, "auto_renew" )
2814 if $now >= dt_from_string( $issue->date_due, 'sql' );
2815 return ( 0, "auto_too_soon" );
2818 return ( 1, undef );
2821 =head2 AddRenewal
2823 &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
2825 Renews a loan.
2827 C<$borrowernumber> is the borrower number of the patron who currently
2828 has the item.
2830 C<$itemnumber> is the number of the item to renew.
2832 C<$branch> is the library where the renewal took place (if any).
2833 The library that controls the circ policies for the renewal is retrieved from the issues record.
2835 C<$datedue> can be a DateTime object used to set the due date.
2837 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate. If
2838 this parameter is not supplied, lastreneweddate is set to the current date.
2840 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2841 from the book's item type.
2843 =cut
2845 sub AddRenewal {
2846 my $borrowernumber = shift;
2847 my $itemnumber = shift or return;
2848 my $branch = shift;
2849 my $datedue = shift;
2850 my $lastreneweddate = shift || DateTime->now(time_zone => C4::Context->tz);
2852 my $item_object = Koha::Items->find($itemnumber) or return;
2853 my $biblio = $item_object->biblio;
2854 my $issue = $item_object->checkout;
2855 my $item_unblessed = $item_object->unblessed;
2857 my $dbh = C4::Context->dbh;
2859 return unless $issue;
2861 $borrowernumber ||= $issue->borrowernumber;
2863 if ( defined $datedue && ref $datedue ne 'DateTime' ) {
2864 carp 'Invalid date passed to AddRenewal.';
2865 return;
2868 my $patron = Koha::Patrons->find( $borrowernumber ) or return; # FIXME Should do more than just return
2869 my $patron_unblessed = $patron->unblessed;
2871 my $circ_library = Koha::Libraries->find( _GetCircControlBranch($item_unblessed, $patron_unblessed) );
2873 my $schema = Koha::Database->schema;
2874 $schema->txn_do(sub{
2876 if ( C4::Context->preference('CalculateFinesOnReturn') && $issue->is_overdue ) {
2877 _CalculateAndUpdateFine( { issue => $issue, item => $item_unblessed, borrower => $patron_unblessed } );
2879 _FixOverduesOnReturn( $borrowernumber, $itemnumber, undef, 'RENEWED' );
2881 # If the due date wasn't specified, calculate it by adding the
2882 # book's loan length to today's date or the current due date
2883 # based on the value of the RenewalPeriodBase syspref.
2884 my $itemtype = $item_object->effective_itemtype;
2885 unless ($datedue) {
2887 $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
2888 dt_from_string( $issue->date_due, 'sql' ) :
2889 DateTime->now( time_zone => C4::Context->tz());
2890 $datedue = CalcDateDue($datedue, $itemtype, $circ_library->branchcode, $patron_unblessed, 'is a renewal');
2893 my $fees = Koha::Charges::Fees->new(
2895 patron => $patron,
2896 library => $circ_library,
2897 item => $item_object,
2898 from_date => dt_from_string( $issue->date_due, 'sql' ),
2899 to_date => dt_from_string($datedue),
2903 # Update the issues record to have the new due date, and a new count
2904 # of how many times it has been renewed.
2905 my $renews = $issue->renewals + 1;
2906 my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
2907 WHERE borrowernumber=?
2908 AND itemnumber=?"
2911 $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
2913 # Update the renewal count on the item, and tell zebra to reindex
2914 $renews = $item_object->renewals + 1;
2915 ModItem( { renewals => $renews, onloan => $datedue->strftime('%Y-%m-%d %H:%M')}, $item_object->biblionumber, $itemnumber, { log_action => 0 } );
2917 # Charge a new rental fee, if applicable
2918 my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
2919 if ( $charge > 0 ) {
2920 AddIssuingCharge($issue, $charge, 'RENT_RENEW');
2923 # Charge a new accumulate rental fee, if applicable
2924 my $itemtype_object = Koha::ItemTypes->find( $itemtype );
2925 if ( $itemtype_object ) {
2926 my $accumulate_charge = $fees->accumulate_rentalcharge();
2927 if ( $accumulate_charge > 0 ) {
2928 AddIssuingCharge( $issue, $accumulate_charge, 'RENT_DAILY_RENEW' )
2930 $charge += $accumulate_charge;
2933 # Send a renewal slip according to checkout alert preferencei
2934 if ( C4::Context->preference('RenewalSendNotice') eq '1' ) {
2935 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2936 my %conditions = (
2937 branchcode => $branch,
2938 categorycode => $patron->categorycode,
2939 item_type => $itemtype,
2940 notification => 'CHECKOUT',
2942 if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
2943 SendCirculationAlert(
2945 type => 'RENEWAL',
2946 item => $item_unblessed,
2947 borrower => $patron->unblessed,
2948 branch => $branch,
2954 # Remove any OVERDUES related debarment if the borrower has no overdues
2955 if ( $patron
2956 && $patron->is_debarred
2957 && ! $patron->has_overdues
2958 && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2960 DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2963 unless ( C4::Context->interface eq 'opac' ) { #if from opac we are obeying OpacRenewalBranch as calculated in opac-renew.pl
2964 $branch = C4::Context->userenv ? C4::Context->userenv->{branch} : $branch;
2967 # Add the renewal to stats
2968 UpdateStats(
2970 branch => $branch,
2971 type => 'renew',
2972 amount => $charge,
2973 itemnumber => $itemnumber,
2974 itemtype => $itemtype,
2975 location => $item_object->location,
2976 borrowernumber => $borrowernumber,
2977 ccode => $item_object->ccode,
2981 #Log the renewal
2982 logaction("CIRCULATION", "RENEWAL", $borrowernumber, $itemnumber) if C4::Context->preference("RenewalLog");
2985 return $datedue;
2988 sub GetRenewCount {
2989 # check renewal status
2990 my ( $bornum, $itemno ) = @_;
2991 my $dbh = C4::Context->dbh;
2992 my $renewcount = 0;
2993 my $renewsallowed = 0;
2994 my $renewsleft = 0;
2996 my $patron = Koha::Patrons->find( $bornum );
2997 my $item = Koha::Items->find($itemno);
2999 return (0, 0, 0) unless $patron or $item; # Wrong call, no renewal allowed
3001 # Look in the issues table for this item, lent to this borrower,
3002 # and not yet returned.
3004 # FIXME - I think this function could be redone to use only one SQL call.
3005 my $sth = $dbh->prepare(
3006 "select * from issues
3007 where (borrowernumber = ?)
3008 and (itemnumber = ?)"
3010 $sth->execute( $bornum, $itemno );
3011 my $data = $sth->fetchrow_hashref;
3012 $renewcount = $data->{'renewals'} if $data->{'renewals'};
3013 # $item and $borrower should be calculated
3014 my $branchcode = _GetCircControlBranch($item->unblessed, $patron->unblessed);
3016 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
3017 { categorycode => $patron->categorycode,
3018 itemtype => $item->effective_itemtype,
3019 branchcode => $branchcode
3023 $renewsallowed = $issuing_rule ? $issuing_rule->renewalsallowed : 0;
3024 $renewsleft = $renewsallowed - $renewcount;
3025 if($renewsleft < 0){ $renewsleft = 0; }
3026 return ( $renewcount, $renewsallowed, $renewsleft );
3029 =head2 GetSoonestRenewDate
3031 $NoRenewalBeforeThisDate = &GetSoonestRenewDate($borrowernumber, $itemnumber);
3033 Find out the soonest possible renew date of a borrowed item.
3035 C<$borrowernumber> is the borrower number of the patron who currently
3036 has the item on loan.
3038 C<$itemnumber> is the number of the item to renew.
3040 C<$GetSoonestRenewDate> returns the DateTime of the soonest possible
3041 renew date, based on the value "No renewal before" of the applicable
3042 issuing rule. Returns the current date if the item can already be
3043 renewed, and returns undefined if the borrower, loan, or item
3044 cannot be found.
3046 =cut
3048 sub GetSoonestRenewDate {
3049 my ( $borrowernumber, $itemnumber ) = @_;
3051 my $dbh = C4::Context->dbh;
3053 my $item = Koha::Items->find($itemnumber) or return;
3054 my $itemissue = $item->checkout or return;
3056 $borrowernumber ||= $itemissue->borrowernumber;
3057 my $patron = Koha::Patrons->find( $borrowernumber )
3058 or return;
3060 my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
3061 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
3062 { categorycode => $patron->categorycode,
3063 itemtype => $item->effective_itemtype,
3064 branchcode => $branchcode
3068 my $now = dt_from_string;
3069 return $now unless $issuing_rule;
3071 if ( defined $issuing_rule->norenewalbefore
3072 and $issuing_rule->norenewalbefore ne "" )
3074 my $soonestrenewal =
3075 dt_from_string( $itemissue->date_due )->subtract(
3076 $issuing_rule->lengthunit => $issuing_rule->norenewalbefore );
3078 if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
3079 and $issuing_rule->lengthunit eq 'days' )
3081 $soonestrenewal->truncate( to => 'day' );
3083 return $soonestrenewal if $now < $soonestrenewal;
3085 return $now;
3088 =head2 GetLatestAutoRenewDate
3090 $NoAutoRenewalAfterThisDate = &GetLatestAutoRenewDate($borrowernumber, $itemnumber);
3092 Find out the latest possible auto renew date of a borrowed item.
3094 C<$borrowernumber> is the borrower number of the patron who currently
3095 has the item on loan.
3097 C<$itemnumber> is the number of the item to renew.
3099 C<$GetLatestAutoRenewDate> returns the DateTime of the latest possible
3100 auto renew date, based on the value "No auto renewal after" and the "No auto
3101 renewal after (hard limit) of the applicable issuing rule.
3102 Returns undef if there is no date specify in the circ rules or if the patron, loan,
3103 or item cannot be found.
3105 =cut
3107 sub GetLatestAutoRenewDate {
3108 my ( $borrowernumber, $itemnumber ) = @_;
3110 my $dbh = C4::Context->dbh;
3112 my $item = Koha::Items->find($itemnumber) or return;
3113 my $itemissue = $item->checkout or return;
3115 $borrowernumber ||= $itemissue->borrowernumber;
3116 my $patron = Koha::Patrons->find( $borrowernumber )
3117 or return;
3119 my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
3120 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
3121 { categorycode => $patron->categorycode,
3122 itemtype => $item->effective_itemtype,
3123 branchcode => $branchcode
3127 return unless $issuing_rule;
3128 return
3129 if ( not $issuing_rule->no_auto_renewal_after
3130 or $issuing_rule->no_auto_renewal_after eq '' )
3131 and ( not $issuing_rule->no_auto_renewal_after_hard_limit
3132 or $issuing_rule->no_auto_renewal_after_hard_limit eq '' );
3134 my $maximum_renewal_date;
3135 if ( $issuing_rule->no_auto_renewal_after ) {
3136 $maximum_renewal_date = dt_from_string($itemissue->issuedate);
3137 $maximum_renewal_date->add(
3138 $issuing_rule->lengthunit => $issuing_rule->no_auto_renewal_after
3142 if ( $issuing_rule->no_auto_renewal_after_hard_limit ) {
3143 my $dt = dt_from_string( $issuing_rule->no_auto_renewal_after_hard_limit );
3144 $maximum_renewal_date = $dt if not $maximum_renewal_date or $maximum_renewal_date > $dt;
3146 return $maximum_renewal_date;
3150 =head2 GetIssuingCharges
3152 ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
3154 Calculate how much it would cost for a given patron to borrow a given
3155 item, including any applicable discounts.
3157 C<$itemnumber> is the item number of item the patron wishes to borrow.
3159 C<$borrowernumber> is the patron's borrower number.
3161 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
3162 and C<$item_type> is the code for the item's item type (e.g., C<VID>
3163 if it's a video).
3165 =cut
3167 sub GetIssuingCharges {
3169 # calculate charges due
3170 my ( $itemnumber, $borrowernumber ) = @_;
3171 my $charge = 0;
3172 my $dbh = C4::Context->dbh;
3173 my $item_type;
3175 # Get the book's item type and rental charge (via its biblioitem).
3176 my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
3177 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
3178 $charge_query .= (C4::Context->preference('item-level_itypes'))
3179 ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
3180 : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
3182 $charge_query .= ' WHERE items.itemnumber =?';
3184 my $sth = $dbh->prepare($charge_query);
3185 $sth->execute($itemnumber);
3186 if ( my $item_data = $sth->fetchrow_hashref ) {
3187 $item_type = $item_data->{itemtype};
3188 $charge = $item_data->{rentalcharge};
3189 my $branch = C4::Context::mybranch();
3190 my $discount_query = q|SELECT rentaldiscount,
3191 issuingrules.itemtype, issuingrules.branchcode
3192 FROM borrowers
3193 LEFT JOIN issuingrules ON borrowers.categorycode = issuingrules.categorycode
3194 WHERE borrowers.borrowernumber = ?
3195 AND (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
3196 AND (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')|;
3197 my $discount_sth = $dbh->prepare($discount_query);
3198 $discount_sth->execute( $borrowernumber, $item_type, $branch );
3199 my $discount_rules = $discount_sth->fetchall_arrayref({});
3200 if (@{$discount_rules}) {
3201 # We may have multiple rules so get the most specific
3202 my $discount = _get_discount_from_rule($discount_rules, $branch, $item_type);
3203 $charge = ( $charge * ( 100 - $discount ) ) / 100;
3205 if ($charge) {
3206 $charge = sprintf '%.2f', $charge; # ensure no fractions of a penny returned
3210 return ( $charge, $item_type );
3213 # Select most appropriate discount rule from those returned
3214 sub _get_discount_from_rule {
3215 my ($rules_ref, $branch, $itemtype) = @_;
3216 my $discount;
3218 if (@{$rules_ref} == 1) { # only 1 applicable rule use it
3219 $discount = $rules_ref->[0]->{rentaldiscount};
3220 return (defined $discount) ? $discount : 0;
3222 # could have up to 4 does one match $branch and $itemtype
3223 my @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq $itemtype } @{$rules_ref};
3224 if (@d) {
3225 $discount = $d[0]->{rentaldiscount};
3226 return (defined $discount) ? $discount : 0;
3228 # do we have item type + all branches
3229 @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq $itemtype } @{$rules_ref};
3230 if (@d) {
3231 $discount = $d[0]->{rentaldiscount};
3232 return (defined $discount) ? $discount : 0;
3234 # do we all item types + this branch
3235 @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq q{*} } @{$rules_ref};
3236 if (@d) {
3237 $discount = $d[0]->{rentaldiscount};
3238 return (defined $discount) ? $discount : 0;
3240 # so all and all (surely we wont get here)
3241 @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq q{*} } @{$rules_ref};
3242 if (@d) {
3243 $discount = $d[0]->{rentaldiscount};
3244 return (defined $discount) ? $discount : 0;
3246 # none of the above
3247 return 0;
3250 =head2 AddIssuingCharge
3252 &AddIssuingCharge( $checkout, $charge, $type )
3254 =cut
3256 sub AddIssuingCharge {
3257 my ( $checkout, $charge, $type ) = @_;
3259 # FIXME What if checkout does not exist?
3261 my $account = Koha::Account->new({ patron_id => $checkout->borrowernumber });
3262 my $accountline = $account->add_debit(
3264 amount => $charge,
3265 note => undef,
3266 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
3267 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
3268 interface => C4::Context->interface,
3269 type => $type,
3270 item_id => $checkout->itemnumber,
3271 issue_id => $checkout->issue_id,
3276 =head2 GetTransfers
3278 GetTransfers($itemnumber);
3280 =cut
3282 sub GetTransfers {
3283 my ($itemnumber) = @_;
3285 my $dbh = C4::Context->dbh;
3287 my $query = '
3288 SELECT datesent,
3289 frombranch,
3290 tobranch,
3291 branchtransfer_id
3292 FROM branchtransfers
3293 WHERE itemnumber = ?
3294 AND datearrived IS NULL
3296 my $sth = $dbh->prepare($query);
3297 $sth->execute($itemnumber);
3298 my @row = $sth->fetchrow_array();
3299 return @row;
3302 =head2 GetTransfersFromTo
3304 @results = GetTransfersFromTo($frombranch,$tobranch);
3306 Returns the list of pending transfers between $from and $to branch
3308 =cut
3310 sub GetTransfersFromTo {
3311 my ( $frombranch, $tobranch ) = @_;
3312 return unless ( $frombranch && $tobranch );
3313 my $dbh = C4::Context->dbh;
3314 my $query = "
3315 SELECT branchtransfer_id,itemnumber,datesent,frombranch
3316 FROM branchtransfers
3317 WHERE frombranch=?
3318 AND tobranch=?
3319 AND datearrived IS NULL
3321 my $sth = $dbh->prepare($query);
3322 $sth->execute( $frombranch, $tobranch );
3323 my @gettransfers;
3325 while ( my $data = $sth->fetchrow_hashref ) {
3326 push @gettransfers, $data;
3328 return (@gettransfers);
3331 =head2 DeleteTransfer
3333 &DeleteTransfer($itemnumber);
3335 =cut
3337 sub DeleteTransfer {
3338 my ($itemnumber) = @_;
3339 return unless $itemnumber;
3340 my $dbh = C4::Context->dbh;
3341 my $sth = $dbh->prepare(
3342 "DELETE FROM branchtransfers
3343 WHERE itemnumber=?
3344 AND datearrived IS NULL "
3346 return $sth->execute($itemnumber);
3349 =head2 SendCirculationAlert
3351 Send out a C<check-in> or C<checkout> alert using the messaging system.
3353 B<Parameters>:
3355 =over 4
3357 =item type
3359 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
3361 =item item
3363 Hashref of information about the item being checked in or out.
3365 =item borrower
3367 Hashref of information about the borrower of the item.
3369 =item branch
3371 The branchcode from where the checkout or check-in took place.
3373 =back
3375 B<Example>:
3377 SendCirculationAlert({
3378 type => 'CHECKOUT',
3379 item => $item,
3380 borrower => $borrower,
3381 branch => $branch,
3384 =cut
3386 sub SendCirculationAlert {
3387 my ($opts) = @_;
3388 my ($type, $item, $borrower, $branch) =
3389 ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
3390 my %message_name = (
3391 CHECKIN => 'Item_Check_in',
3392 CHECKOUT => 'Item_Checkout',
3393 RENEWAL => 'Item_Checkout',
3395 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
3396 borrowernumber => $borrower->{borrowernumber},
3397 message_name => $message_name{$type},
3399 my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues';
3401 my $schema = Koha::Database->new->schema;
3402 my @transports = keys %{ $borrower_preferences->{transports} };
3404 # From the MySQL doc:
3405 # LOCK TABLES is not transaction-safe and implicitly commits any active transaction before attempting to lock the tables.
3406 # If the LOCK/UNLOCK statements are executed from tests, the current transaction will be committed.
3407 # To avoid that we need to guess if this code is execute from tests or not (yes it is a bit hacky)
3408 my $do_not_lock = ( exists $ENV{_} && $ENV{_} =~ m|prove| ) || $ENV{KOHA_NO_TABLE_LOCKS};
3410 for my $mtt (@transports) {
3411 my $letter = C4::Letters::GetPreparedLetter (
3412 module => 'circulation',
3413 letter_code => $type,
3414 branchcode => $branch,
3415 message_transport_type => $mtt,
3416 lang => $borrower->{lang},
3417 tables => {
3418 $issues_table => $item->{itemnumber},
3419 'items' => $item->{itemnumber},
3420 'biblio' => $item->{biblionumber},
3421 'biblioitems' => $item->{biblionumber},
3422 'borrowers' => $borrower,
3423 'branches' => $branch,
3425 ) or next;
3427 $schema->storage->txn_begin;
3428 C4::Context->dbh->do(q|LOCK TABLE message_queue READ|) unless $do_not_lock;
3429 C4::Context->dbh->do(q|LOCK TABLE message_queue WRITE|) unless $do_not_lock;
3430 my $message = C4::Message->find_last_message($borrower, $type, $mtt);
3431 unless ( $message ) {
3432 C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3433 C4::Message->enqueue($letter, $borrower, $mtt);
3434 } else {
3435 $message->append($letter);
3436 $message->update;
3438 C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3439 $schema->storage->txn_commit;
3442 return;
3445 =head2 updateWrongTransfer
3447 $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
3449 This function validate the line of brachtransfer but with the wrong destination (mistake from a librarian ...), and create a new line in branchtransfer from the actual library to the original library of reservation
3451 =cut
3453 sub updateWrongTransfer {
3454 my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
3455 my $dbh = C4::Context->dbh;
3456 # first step validate the actual line of transfert .
3457 my $sth =
3458 $dbh->prepare(
3459 "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
3461 $sth->execute($FromLibrary,$itemNumber);
3463 # second step create a new line of branchtransfer to the right location .
3464 ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
3466 #third step changing holdingbranch of item
3467 UpdateHoldingbranch($FromLibrary,$itemNumber);
3470 =head2 UpdateHoldingbranch
3472 $items = UpdateHoldingbranch($branch,$itmenumber);
3474 Simple methode for updating hodlingbranch in items BDD line
3476 =cut
3478 sub UpdateHoldingbranch {
3479 my ( $branch,$itemnumber ) = @_;
3480 ModItem({ holdingbranch => $branch }, undef, $itemnumber);
3483 =head2 CalcDateDue
3485 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
3487 this function calculates the due date given the start date and configured circulation rules,
3488 checking against the holidays calendar as per the 'useDaysMode' syspref.
3489 C<$startdate> = DateTime object representing start date of loan period (assumed to be today)
3490 C<$itemtype> = itemtype code of item in question
3491 C<$branch> = location whose calendar to use
3492 C<$borrower> = Borrower object
3493 C<$isrenewal> = Boolean: is true if we want to calculate the date due for a renewal. Else is false.
3495 =cut
3497 sub CalcDateDue {
3498 my ( $startdate, $itemtype, $branch, $borrower, $isrenewal ) = @_;
3500 $isrenewal ||= 0;
3502 # loanlength now a href
3503 my $loanlength =
3504 GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
3506 my $length_key = ( $isrenewal and defined $loanlength->{renewalperiod} )
3507 ? qq{renewalperiod}
3508 : qq{issuelength};
3510 my $datedue;
3511 if ( $startdate ) {
3512 if (ref $startdate ne 'DateTime' ) {
3513 $datedue = dt_from_string($datedue);
3514 } else {
3515 $datedue = $startdate->clone;
3517 } else {
3518 $datedue =
3519 DateTime->now( time_zone => C4::Context->tz() )
3520 ->truncate( to => 'minute' );
3524 # calculate the datedue as normal
3525 if ( C4::Context->preference('useDaysMode') eq 'Days' )
3526 { # ignoring calendar
3527 if ( $loanlength->{lengthunit} eq 'hours' ) {
3528 $datedue->add( hours => $loanlength->{$length_key} );
3529 } else { # days
3530 $datedue->add( days => $loanlength->{$length_key} );
3531 $datedue->set_hour(23);
3532 $datedue->set_minute(59);
3534 } else {
3535 my $dur;
3536 if ($loanlength->{lengthunit} eq 'hours') {
3537 $dur = DateTime::Duration->new( hours => $loanlength->{$length_key});
3539 else { # days
3540 $dur = DateTime::Duration->new( days => $loanlength->{$length_key});
3542 my $calendar = Koha::Calendar->new( branchcode => $branch );
3543 $datedue = $calendar->addDate( $datedue, $dur, $loanlength->{lengthunit} );
3544 if ($loanlength->{lengthunit} eq 'days') {
3545 $datedue->set_hour(23);
3546 $datedue->set_minute(59);
3550 # if Hard Due Dates are used, retrieve them and apply as necessary
3551 my ( $hardduedate, $hardduedatecompare ) =
3552 GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
3553 if ($hardduedate) { # hardduedates are currently dates
3554 $hardduedate->truncate( to => 'minute' );
3555 $hardduedate->set_hour(23);
3556 $hardduedate->set_minute(59);
3557 my $cmp = DateTime->compare( $hardduedate, $datedue );
3559 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
3560 # if the calculated date is before the 'after' Hard Due Date (floor), override
3561 # if the hard due date is set to 'exactly', overrride
3562 if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
3563 $datedue = $hardduedate->clone;
3566 # in all other cases, keep the date due as it is
3570 # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3571 if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3572 my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso', 'floating');
3573 if( $expiry_dt ) { #skip empty expiry date..
3574 $expiry_dt->set( hour => 23, minute => 59);
3575 my $d1= $datedue->clone->set_time_zone('floating');
3576 if ( DateTime->compare( $d1, $expiry_dt ) == 1 ) {
3577 $datedue = $expiry_dt->clone->set_time_zone( C4::Context->tz );
3580 if ( C4::Context->preference('useDaysMode') ne 'Days' ) {
3581 my $calendar = Koha::Calendar->new( branchcode => $branch );
3582 if ( $calendar->is_holiday($datedue) ) {
3583 # Don't return on a closed day
3584 $datedue = $calendar->prev_open_day( $datedue );
3589 return $datedue;
3593 sub CheckValidBarcode{
3594 my ($barcode) = @_;
3595 my $dbh = C4::Context->dbh;
3596 my $query=qq|SELECT count(*)
3597 FROM items
3598 WHERE barcode=?
3600 my $sth = $dbh->prepare($query);
3601 $sth->execute($barcode);
3602 my $exist=$sth->fetchrow ;
3603 return $exist;
3606 =head2 IsBranchTransferAllowed
3608 $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3610 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3612 Deprecated in favor of Koha::Item::Transfer::Limits->find/search and
3613 Koha::Item->can_be_transferred.
3615 =cut
3617 sub IsBranchTransferAllowed {
3618 my ( $toBranch, $fromBranch, $code ) = @_;
3620 if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3622 my $limitType = C4::Context->preference("BranchTransferLimitsType");
3623 my $dbh = C4::Context->dbh;
3625 my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3626 $sth->execute( $toBranch, $fromBranch, $code );
3627 my $limit = $sth->fetchrow_hashref();
3629 ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3630 if ( $limit->{'limitId'} ) {
3631 return 0;
3632 } else {
3633 return 1;
3637 =head2 CreateBranchTransferLimit
3639 CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3641 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3643 Deprecated in favor of Koha::Item::Transfer::Limit->new.
3645 =cut
3647 sub CreateBranchTransferLimit {
3648 my ( $toBranch, $fromBranch, $code ) = @_;
3649 return unless defined($toBranch) && defined($fromBranch);
3650 my $limitType = C4::Context->preference("BranchTransferLimitsType");
3652 my $dbh = C4::Context->dbh;
3654 my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3655 return $sth->execute( $code, $toBranch, $fromBranch );
3658 =head2 DeleteBranchTransferLimits
3660 my $result = DeleteBranchTransferLimits($frombranch);
3662 Deletes all the library transfer limits for one library. Returns the
3663 number of limits deleted, 0e0 if no limits were deleted, or undef if
3664 no arguments are supplied.
3666 Deprecated in favor of Koha::Item::Transfer::Limits->search({
3667 fromBranch => $fromBranch
3668 })->delete.
3670 =cut
3672 sub DeleteBranchTransferLimits {
3673 my $branch = shift;
3674 return unless defined $branch;
3675 my $dbh = C4::Context->dbh;
3676 my $sth = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3677 return $sth->execute($branch);
3680 sub ReturnLostItem{
3681 my ( $borrowernumber, $itemnum ) = @_;
3682 MarkIssueReturned( $borrowernumber, $itemnum );
3686 sub LostItem{
3687 my ($itemnumber, $mark_lost_from, $force_mark_returned) = @_;
3689 unless ( $mark_lost_from ) {
3690 # Temporary check to avoid regressions
3691 die q|LostItem called without $mark_lost_from, check the API.|;
3694 my $mark_returned;
3695 if ( $force_mark_returned ) {
3696 $mark_returned = 1;
3697 } else {
3698 my $pref = C4::Context->preference('MarkLostItemsAsReturned') // q{};
3699 $mark_returned = ( $pref =~ m|$mark_lost_from| );
3702 my $dbh = C4::Context->dbh();
3703 my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title
3704 FROM issues
3705 JOIN items USING (itemnumber)
3706 JOIN biblio USING (biblionumber)
3707 WHERE issues.itemnumber=?");
3708 $sth->execute($itemnumber);
3709 my $issues=$sth->fetchrow_hashref();
3711 # If a borrower lost the item, add a replacement cost to the their record
3712 if ( my $borrowernumber = $issues->{borrowernumber} ){
3713 my $patron = Koha::Patrons->find( $borrowernumber );
3715 my $fix = _FixOverduesOnReturn($borrowernumber, $itemnumber, C4::Context->preference('WhenLostForgiveFine'), 'LOST');
3716 defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $itemnumber...) failed!"; # zero is OK, check defined
3718 if (C4::Context->preference('WhenLostChargeReplacementFee')){
3719 C4::Accounts::chargelostitem($borrowernumber, $itemnumber, $issues->{'replacementprice'}, "$issues->{'title'} $issues->{'barcode'} $issues->{'itemcallnumber'}");
3720 #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3721 #warn " $issues->{'borrowernumber'} / $itemnumber ";
3724 MarkIssueReturned($borrowernumber,$itemnumber,undef,$patron->privacy) if $mark_returned;
3727 #When item is marked lost automatically cancel its outstanding transfers and set items holdingbranch to the transfer source branch (frombranch)
3728 if (my ( $datesent,$frombranch,$tobranch ) = GetTransfers($itemnumber)) {
3729 ModItem({holdingbranch => $frombranch}, undef, $itemnumber);
3731 my $transferdeleted = DeleteTransfer($itemnumber);
3734 sub GetOfflineOperations {
3735 my $dbh = C4::Context->dbh;
3736 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3737 $sth->execute(C4::Context->userenv->{'branch'});
3738 my $results = $sth->fetchall_arrayref({});
3739 return $results;
3742 sub GetOfflineOperation {
3743 my $operationid = shift;
3744 return unless $operationid;
3745 my $dbh = C4::Context->dbh;
3746 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3747 $sth->execute( $operationid );
3748 return $sth->fetchrow_hashref;
3751 sub AddOfflineOperation {
3752 my ( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount ) = @_;
3753 my $dbh = C4::Context->dbh;
3754 my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber, amount) VALUES(?,?,?,?,?,?,?)");
3755 $sth->execute( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount );
3756 return "Added.";
3759 sub DeleteOfflineOperation {
3760 my $dbh = C4::Context->dbh;
3761 my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3762 $sth->execute( shift );
3763 return "Deleted.";
3766 sub ProcessOfflineOperation {
3767 my $operation = shift;
3769 my $report;
3770 if ( $operation->{action} eq 'return' ) {
3771 $report = ProcessOfflineReturn( $operation );
3772 } elsif ( $operation->{action} eq 'issue' ) {
3773 $report = ProcessOfflineIssue( $operation );
3774 } elsif ( $operation->{action} eq 'payment' ) {
3775 $report = ProcessOfflinePayment( $operation );
3778 DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3780 return $report;
3783 sub ProcessOfflineReturn {
3784 my $operation = shift;
3786 my $item = Koha::Items->find({barcode => $operation->{barcode}});
3788 if ( $item ) {
3789 my $itemnumber = $item->itemnumber;
3790 my $issue = GetOpenIssue( $itemnumber );
3791 if ( $issue ) {
3792 MarkIssueReturned(
3793 $issue->{borrowernumber},
3794 $itemnumber,
3795 $operation->{timestamp},
3797 ModItem(
3798 { renewals => 0, onloan => undef },
3799 $issue->{'biblionumber'},
3800 $itemnumber,
3801 { log_action => 0 }
3803 return "Success.";
3804 } else {
3805 return "Item not issued.";
3807 } else {
3808 return "Item not found.";
3812 sub ProcessOfflineIssue {
3813 my $operation = shift;
3815 my $patron = Koha::Patrons->find( { cardnumber => $operation->{cardnumber} } );
3817 if ( $patron ) {
3818 my $item = Koha::Items->find({ barcode => $operation->{barcode} });
3819 unless ($item) {
3820 return "Barcode not found.";
3822 my $itemnumber = $item->itemnumber;
3823 my $issue = GetOpenIssue( $itemnumber );
3825 if ( $issue and ( $issue->{borrowernumber} ne $patron->borrowernumber ) ) { # Item already issued to another patron mark it returned
3826 MarkIssueReturned(
3827 $issue->{borrowernumber},
3828 $itemnumber,
3829 $operation->{timestamp},
3832 AddIssue(
3833 $patron->unblessed,
3834 $operation->{'barcode'},
3835 undef,
3837 $operation->{timestamp},
3838 undef,
3840 return "Success.";
3841 } else {
3842 return "Borrower not found.";
3846 sub ProcessOfflinePayment {
3847 my $operation = shift;
3849 my $patron = Koha::Patrons->find({ cardnumber => $operation->{cardnumber} });
3851 $patron->account->pay(
3853 amount => $operation->{amount},
3854 library_id => $operation->{branchcode},
3855 interface => 'koc'
3859 return "Success.";
3862 =head2 TransferSlip
3864 TransferSlip($user_branch, $itemnumber, $barcode, $to_branch)
3866 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
3868 =cut
3870 sub TransferSlip {
3871 my ($branch, $itemnumber, $barcode, $to_branch) = @_;
3873 my $item =
3874 $itemnumber
3875 ? Koha::Items->find($itemnumber)
3876 : Koha::Items->find( { barcode => $barcode } );
3878 $item or return;
3880 return C4::Letters::GetPreparedLetter (
3881 module => 'circulation',
3882 letter_code => 'TRANSFERSLIP',
3883 branchcode => $branch,
3884 tables => {
3885 'branches' => $to_branch,
3886 'biblio' => $item->biblionumber,
3887 'items' => $item->unblessed,
3892 =head2 CheckIfIssuedToPatron
3894 CheckIfIssuedToPatron($borrowernumber, $biblionumber)
3896 Return 1 if any record item is issued to patron, otherwise return 0
3898 =cut
3900 sub CheckIfIssuedToPatron {
3901 my ($borrowernumber, $biblionumber) = @_;
3903 my $dbh = C4::Context->dbh;
3904 my $query = q|
3905 SELECT COUNT(*) FROM issues
3906 LEFT JOIN items ON items.itemnumber = issues.itemnumber
3907 WHERE items.biblionumber = ?
3908 AND issues.borrowernumber = ?
3910 my $is_issued = $dbh->selectrow_array($query, {}, $biblionumber, $borrowernumber );
3911 return 1 if $is_issued;
3912 return;
3915 =head2 IsItemIssued
3917 IsItemIssued( $itemnumber )
3919 Return 1 if the item is on loan, otherwise return 0
3921 =cut
3923 sub IsItemIssued {
3924 my $itemnumber = shift;
3925 my $dbh = C4::Context->dbh;
3926 my $sth = $dbh->prepare(q{
3927 SELECT COUNT(*)
3928 FROM issues
3929 WHERE itemnumber = ?
3931 $sth->execute($itemnumber);
3932 return $sth->fetchrow;
3935 =head2 GetAgeRestriction
3937 my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions, $borrower);
3938 my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions);
3940 if($daysToAgeRestriction <= 0) { #Borrower is allowed to access this material, as they are older or as old as the agerestriction }
3941 if($daysToAgeRestriction > 0) { #Borrower is this many days from meeting the agerestriction }
3943 @PARAM1 the koha.biblioitems.agerestriction value, like K18, PEGI 13, ...
3944 @PARAM2 a borrower-object with koha.borrowers.dateofbirth. (OPTIONAL)
3945 @RETURNS The age restriction age in years and the days to fulfill the age restriction for the given borrower.
3946 Negative days mean the borrower has gone past the age restriction age.
3948 =cut
3950 sub GetAgeRestriction {
3951 my ($record_restrictions, $borrower) = @_;
3952 my $markers = C4::Context->preference('AgeRestrictionMarker');
3954 # Split $record_restrictions to something like FSK 16 or PEGI 6
3955 my @values = split ' ', uc($record_restrictions);
3956 return unless @values;
3958 # Search first occurrence of one of the markers
3959 my @markers = split /\|/, uc($markers);
3960 return unless @markers;
3962 my $index = 0;
3963 my $restriction_year = 0;
3964 for my $value (@values) {
3965 $index++;
3966 for my $marker (@markers) {
3967 $marker =~ s/^\s+//; #remove leading spaces
3968 $marker =~ s/\s+$//; #remove trailing spaces
3969 if ( $marker eq $value ) {
3970 if ( $index <= $#values ) {
3971 $restriction_year += $values[$index];
3973 last;
3975 elsif ( $value =~ /^\Q$marker\E(\d+)$/ ) {
3977 # Perhaps it is something like "K16" (as in Finland)
3978 $restriction_year += $1;
3979 last;
3982 last if ( $restriction_year > 0 );
3985 #Check if the borrower is age restricted for this material and for how long.
3986 if ($restriction_year && $borrower) {
3987 if ( $borrower->{'dateofbirth'} ) {
3988 my @alloweddate = split /-/, $borrower->{'dateofbirth'};
3989 $alloweddate[0] += $restriction_year;
3991 #Prevent runime eror on leap year (invalid date)
3992 if ( ( $alloweddate[1] == 2 ) && ( $alloweddate[2] == 29 ) ) {
3993 $alloweddate[2] = 28;
3996 #Get how many days the borrower has to reach the age restriction
3997 my @Today = split /-/, DateTime->today->ymd();
3998 my $daysToAgeRestriction = Date_to_Days(@alloweddate) - Date_to_Days(@Today);
3999 #Negative days means the borrower went past the age restriction age
4000 return ($restriction_year, $daysToAgeRestriction);
4004 return ($restriction_year);
4008 =head2 GetPendingOnSiteCheckouts
4010 =cut
4012 sub GetPendingOnSiteCheckouts {
4013 my $dbh = C4::Context->dbh;
4014 return $dbh->selectall_arrayref(q|
4015 SELECT
4016 items.barcode,
4017 items.biblionumber,
4018 items.itemnumber,
4019 items.itemnotes,
4020 items.itemcallnumber,
4021 items.location,
4022 issues.date_due,
4023 issues.branchcode,
4024 issues.date_due < NOW() AS is_overdue,
4025 biblio.author,
4026 biblio.title,
4027 borrowers.firstname,
4028 borrowers.surname,
4029 borrowers.cardnumber,
4030 borrowers.borrowernumber
4031 FROM items
4032 LEFT JOIN issues ON items.itemnumber = issues.itemnumber
4033 LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
4034 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
4035 WHERE issues.onsite_checkout = 1
4036 |, { Slice => {} } );
4039 sub GetTopIssues {
4040 my ($params) = @_;
4042 my ($count, $branch, $itemtype, $ccode, $newness)
4043 = @$params{qw(count branch itemtype ccode newness)};
4045 my $dbh = C4::Context->dbh;
4046 my $query = q{
4047 SELECT * FROM (
4048 SELECT b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4049 bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4050 i.ccode, SUM(i.issues) AS count
4051 FROM biblio b
4052 LEFT JOIN items i ON (i.biblionumber = b.biblionumber)
4053 LEFT JOIN biblioitems bi ON (bi.biblionumber = b.biblionumber)
4056 my (@where_strs, @where_args);
4058 if ($branch) {
4059 push @where_strs, 'i.homebranch = ?';
4060 push @where_args, $branch;
4062 if ($itemtype) {
4063 if (C4::Context->preference('item-level_itypes')){
4064 push @where_strs, 'i.itype = ?';
4065 push @where_args, $itemtype;
4066 } else {
4067 push @where_strs, 'bi.itemtype = ?';
4068 push @where_args, $itemtype;
4071 if ($ccode) {
4072 push @where_strs, 'i.ccode = ?';
4073 push @where_args, $ccode;
4075 if ($newness) {
4076 push @where_strs, 'TO_DAYS(NOW()) - TO_DAYS(b.datecreated) <= ?';
4077 push @where_args, $newness;
4080 if (@where_strs) {
4081 $query .= 'WHERE ' . join(' AND ', @where_strs);
4084 $query .= q{
4085 GROUP BY b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4086 bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4087 i.ccode
4088 ORDER BY count DESC
4091 $query .= q{ ) xxx WHERE count > 0 };
4092 $count = int($count);
4093 if ($count > 0) {
4094 $query .= "LIMIT $count";
4097 my $rows = $dbh->selectall_arrayref($query, { Slice => {} }, @where_args);
4099 return @$rows;
4102 sub _CalculateAndUpdateFine {
4103 my ($params) = @_;
4105 my $borrower = $params->{borrower};
4106 my $item = $params->{item};
4107 my $issue = $params->{issue};
4108 my $return_date = $params->{return_date};
4110 unless ($borrower) { carp "No borrower passed in!" && return; }
4111 unless ($item) { carp "No item passed in!" && return; }
4112 unless ($issue) { carp "No issue passed in!" && return; }
4114 my $datedue = dt_from_string( $issue->date_due );
4116 # we only need to calculate and change the fines if we want to do that on return
4117 # Should be on for hourly loans
4118 my $control = C4::Context->preference('CircControl');
4119 my $control_branchcode =
4120 ( $control eq 'ItemHomeLibrary' ) ? $item->{homebranch}
4121 : ( $control eq 'PatronLibrary' ) ? $borrower->{branchcode}
4122 : $issue->branchcode;
4124 my $date_returned = $return_date ? $return_date : dt_from_string();
4126 my ( $amount, $unitcounttotal, $unitcount ) =
4127 C4::Overdues::CalcFine( $item, $borrower->{categorycode}, $control_branchcode, $datedue, $date_returned );
4129 if ( C4::Context->preference('finesMode') eq 'production' ) {
4130 if ( $amount > 0 ) {
4131 C4::Overdues::UpdateFine({
4132 issue_id => $issue->issue_id,
4133 itemnumber => $issue->itemnumber,
4134 borrowernumber => $issue->borrowernumber,
4135 amount => $amount,
4136 due => output_pref($datedue),
4139 elsif ($return_date) {
4141 # Backdated returns may have fines that shouldn't exist,
4142 # so in this case, we need to drop those fines to 0
4144 C4::Overdues::UpdateFine({
4145 issue_id => $issue->issue_id,
4146 itemnumber => $issue->itemnumber,
4147 borrowernumber => $issue->borrowernumber,
4148 amount => 0,
4149 due => output_pref($datedue),
4155 sub _item_denied_renewal {
4156 my ($params) = @_;
4158 my $item = $params->{item};
4159 return unless $item;
4161 my $denyingrules = Koha::Config::SysPrefs->find('ItemsDeniedRenewal')->get_yaml_pref_hash();
4162 return unless $denyingrules;
4163 foreach my $field (keys %$denyingrules) {
4164 my $val = $item->$field;
4165 if( !defined $val) {
4166 if ( any { !defined $_ } @{$denyingrules->{$field}} ){
4167 return 1;
4169 } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
4170 # If the results matches the values in the syspref
4171 # We return true if match found
4172 return 1;
4175 return 0;
4181 __END__
4183 =head1 AUTHOR
4185 Koha Development Team <http://koha-community.org/>
4187 =cut