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>.
23 use POSIX qw( floor );
34 use C4::ItemCirculationAlertPreference;
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;
44 use Koha::AuthorisedValues;
45 use Koha::Biblioitems;
49 use Koha::Illrequests;
52 use Koha::Patron::Debarments;
55 use Koha::Account::Lines;
57 use Koha::Account::Lines;
58 use Koha::Account::Offsets;
59 use Koha::Config::SysPrefs;
60 use Koha::Charges::Fees;
61 use Koha::Config::SysPref;
62 use Koha::Checkouts::ReturnClaims;
63 use Koha::SearchEngine::Indexer;
65 use List::MoreUtils qw( uniq any );
66 use Scalar::Util qw( looks_like_number );
77 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
83 # FIXME subs that should probably be elsewhere
88 &GetPendingOnSiteCheckouts
91 # subs to deal with issuing a book
99 &GetLatestAutoRenewDate
101 &GetBranchBorrowerCircRule
104 &CheckIfIssuedToPatron
109 # subs to deal with returns
115 # subs to deal with transfers
122 &IsBranchTransferAllowed
123 &CreateBranchTransferLimit
124 &DeleteBranchTransferLimits
128 # subs to deal with offline circulation
130 &GetOfflineOperations
133 &DeleteOfflineOperation
134 &ProcessOfflineOperation
140 C4::Circulation - Koha circulation module
148 The functions in this module deal with circulation, issues, and
149 returns, as well as general information about the library.
150 Also deals with inventory.
156 $str = &barcodedecode($barcode, [$filter]);
158 Generic filter function for barcode string.
159 Called on every circ if the System Pref itemBarcodeInputFilter is set.
160 Will do some manipulation of the barcode for systems that deliver a barcode
161 to circulation.pl that differs from the barcode stored for the item.
162 For proper functioning of this filter, calling the function on the
163 correct barcode string (items.barcode) should return an unaltered barcode.
165 The optional $filter argument is to allow for testing or explicit
166 behavior that ignores the System Pref. Valid values are the same as the
171 # FIXME -- the &decode fcn below should be wrapped into this one.
172 # FIXME -- these plugins should be moved out of Circulation.pm
175 my ($barcode, $filter) = @_;
176 my $branch = C4::Context::mybranch();
177 $filter = C4::Context->preference('itemBarcodeInputFilter') unless $filter;
178 $filter or return $barcode; # ensure filter is defined, else return untouched barcode
179 if ($filter eq 'whitespace') {
181 } elsif ($filter eq 'cuecat') {
183 my @fields = split( /\./, $barcode );
184 my @results = map( decode($_), @fields[ 1 .. $#fields ] );
185 ($#results == 2) and return $results[2];
186 } elsif ($filter eq 'T-prefix') {
187 if ($barcode =~ /^[Tt](\d)/) {
188 (defined($1) and $1 eq '0') and return $barcode;
189 $barcode = substr($barcode, 2) + 0; # FIXME: probably should be substr($barcode, 1)
191 return sprintf("T%07d", $barcode);
192 # FIXME: $barcode could be "T1", causing warning: substr outside of string
193 # Why drop the nonzero digit after the T?
194 # Why pass non-digits (or empty string) to "T%07d"?
195 } elsif ($filter eq 'libsuite8') {
196 unless($barcode =~ m/^($branch)-/i){ #if barcode starts with branch code its in Koha style. Skip it.
197 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
198 $barcode =~ s/^[0]*(\d+)$/$branch-b-$1/i;
200 $barcode =~ s/^(\D+)[0]*(\d+)$/$branch-$1-$2/i;
203 } elsif ($filter eq 'EAN13') {
204 my $ean = CheckDigits('ean');
205 if ( $ean->is_valid($barcode) ) {
206 #$barcode = sprintf('%013d',$barcode); # this doesn't work on 32-bit systems
207 $barcode = '0' x ( 13 - length($barcode) ) . $barcode;
209 warn "# [$barcode] not valid EAN-13/UPC-A\n";
212 return $barcode; # return barcode, modified or not
217 $str = &decode($chunk);
219 Decodes a segment of a string emitted by a CueCat barcode scanner and
222 FIXME: Should be replaced with Barcode::Cuecat from CPAN
223 or Javascript based decoding on the client side.
230 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-';
231 my @s = map { index( $seq, $_ ); } split( //, $encoded );
232 my $l = ( $#s + 1 ) % 4;
235 # warn "Error: Cuecat decode parsing failed!";
243 my $n = ( ( $s[0] << 6 | $s[1] ) << 6 | $s[2] ) << 6 | $s[3];
245 chr( ( $n >> 16 ) ^ 67 )
246 .chr( ( $n >> 8 & 255 ) ^ 67 )
247 .chr( ( $n & 255 ) ^ 67 );
250 $r = substr( $r, 0, length($r) - $l );
256 ($dotransfer, $messages, $iteminformation) = &transferbook({
257 from_branch => $frombranch
258 to_branch => $tobranch,
260 ignore_reserves => $ignore_reserves,
264 Transfers an item to a new branch. If the item is currently on loan, it is automatically returned before the actual transfer.
266 C<$fbr> is the code for the branch initiating the transfer.
267 C<$tbr> is the code for the branch to which the item should be transferred.
269 C<$barcode> is the barcode of the item to be transferred.
271 If C<$ignore_reserves> is true, C<&transferbook> ignores reserves.
272 Otherwise, if an item is reserved, the transfer fails.
274 C<$trigger> is the enum value for what triggered the transfer.
276 Returns three values:
282 is true if the transfer was successful.
286 is a reference-to-hash which may have any of the following keys:
292 There is no item in the catalog with the given barcode. The value is C<$barcode>.
294 =item C<DestinationEqualsHolding>
296 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.
300 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.
304 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>.
306 =item C<WasTransferred>
308 The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored.
318 my $tbr = $params->{to_branch};
319 my $fbr = $params->{from_branch};
320 my $ignoreRs = $params->{ignore_reserves};
321 my $barcode = $params->{barcode};
322 my $trigger = $params->{trigger};
325 my $item = Koha::Items->find( { barcode => $barcode } );
327 Koha::Exceptions::MissingParameter->throw(
328 "Missing mandatory parameter: from_branch")
331 Koha::Exceptions::MissingParameter->throw(
332 "Missing mandatory parameter: to_branch")
337 $messages->{'BadBarcode'} = $barcode;
339 return ( $dotransfer, $messages );
342 my $itemnumber = $item->itemnumber;
343 # get branches of book...
344 my $hbr = $item->homebranch;
346 # if using Branch Transfer Limits
347 if ( C4::Context->preference("UseBranchTransferLimits") == 1 ) {
348 my $code = C4::Context->preference("BranchTransferLimitsType") eq 'ccode' ? $item->ccode : $item->biblio->biblioitem->itemtype; # BranchTransferLimitsType is 'ccode' or 'itemtype'
349 if ( C4::Context->preference("item-level_itypes") && C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ) {
350 if ( ! IsBranchTransferAllowed( $tbr, $fbr, $item->itype ) ) {
351 $messages->{'NotAllowed'} = $tbr . "::" . $item->itype;
354 } elsif ( ! IsBranchTransferAllowed( $tbr, $fbr, $code ) ) {
355 $messages->{'NotAllowed'} = $tbr . "::" . $code;
360 # can't transfer book if is already there....
361 if ( $fbr eq $tbr ) {
362 $messages->{'DestinationEqualsHolding'} = 1;
366 # check if it is still issued to someone, return it...
367 my $issue = Koha::Checkouts->find({ itemnumber => $itemnumber });
369 AddReturn( $barcode, $fbr );
370 $messages->{'WasReturned'} = $issue->borrowernumber;
374 # That'll save a database query.
375 my ( $resfound, $resrec, undef ) =
376 CheckReserves( $itemnumber );
377 if ( $resfound and not $ignoreRs ) {
378 $resrec->{'ResFound'} = $resfound;
379 $messages->{'ResFound'} = $resrec;
383 #actually do the transfer....
385 ModItemTransfer( $itemnumber, $fbr, $tbr, $trigger );
387 # don't need to update MARC anymore, we do it in batch now
388 $messages->{'WasTransfered'} = 1;
391 ModDateLastSeen( $itemnumber );
392 return ( $dotransfer, $messages );
397 my $borrower = shift;
398 my $item_object = shift;
400 my $onsite_checkout = $params->{onsite_checkout} || 0;
401 my $switch_onsite_checkout = $params->{switch_onsite_checkout} || 0;
402 my $cat_borrower = $borrower->{'categorycode'};
403 my $dbh = C4::Context->dbh;
404 # Get which branchcode we need
405 my $branch = _GetCircControlBranch($item_object->unblessed,$borrower);
406 my $type = $item_object->effective_itemtype;
408 my ($type_object, $parent_type, $parent_maxissueqty_rule);
409 $type_object = Koha::ItemTypes->find( $type );
410 $parent_type = $type_object->parent_type if $type_object;
411 my $child_types = Koha::ItemTypes->search({ parent_type => $type });
412 # Find any children if we are a parent_type;
414 # given branch, patron category, and item type, determine
415 # applicable issuing rule
417 $parent_maxissueqty_rule = Koha::CirculationRules->get_effective_rule(
419 categorycode => $cat_borrower,
420 itemtype => $parent_type,
421 branchcode => $branch,
422 rule_name => 'maxissueqty',
425 # If the parent rule is for default type we discount it
426 $parent_maxissueqty_rule = undef if $parent_maxissueqty_rule && !defined $parent_maxissueqty_rule->itemtype;
428 my $maxissueqty_rule = Koha::CirculationRules->get_effective_rule(
430 categorycode => $cat_borrower,
432 branchcode => $branch,
433 rule_name => 'maxissueqty',
437 my $maxonsiteissueqty_rule = Koha::CirculationRules->get_effective_rule(
439 categorycode => $cat_borrower,
441 branchcode => $branch,
442 rule_name => 'maxonsiteissueqty',
447 my $patron = Koha::Patrons->find($borrower->{borrowernumber});
448 # if a rule is found and has a loan limit set, count
449 # how many loans the patron already has that meet that
451 if (defined($maxissueqty_rule) and $maxissueqty_rule->rule_value ne "") {
454 if ( $maxissueqty_rule->branchcode ) {
455 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' ) {
456 $checkouts = $patron->checkouts->search(
457 { 'me.branchcode' => $maxissueqty_rule->branchcode } );
458 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
459 $checkouts = $patron->checkouts; # if branch is the patron's home branch, then count all loans by patron
461 $checkouts = $patron->checkouts->search(
462 { 'item.homebranch' => $maxissueqty_rule->branchcode },
463 { prefetch => 'item' } );
466 $checkouts = $patron->checkouts; # if rule is not branch specific then count all loans by patron
469 my $rule_itemtype = $maxissueqty_rule->itemtype;
470 while ( my $c = $checkouts->next ) {
471 my $itemtype = $c->item->effective_itemtype;
473 unless ( $rule_itemtype ) {
474 # matching rule has the default item type, so count only
475 # those existing loans that don't fall under a more
477 @types = Koha::CirculationRules->search(
479 branchcode => $maxissueqty_rule->branchcode,
480 categorycode => [ $maxissueqty_rule->categorycode, $cat_borrower ],
481 itemtype => { '!=' => undef },
482 rule_name => 'maxissueqty'
484 )->get_column('itemtype');
486 next if grep {$_ eq $itemtype} @types;
489 if ( $parent_maxissueqty_rule ) {
490 # if we have a parent item type then we count loans of the
491 # specific item type or its siblings or parent
492 my $children = Koha::ItemTypes->search({ parent_type => $parent_type });
493 @types = $children->get_column('itemtype');
494 push @types, $parent_type;
495 } elsif ( $child_types ) {
496 # If we are a parent type, we need to count all child types and our own type
497 @types = $child_types->get_column('itemtype');
498 push @types, $type; # And don't forget to count our own types
499 } else { push @types, $type; } # Otherwise only count the specific itemtype
501 next unless grep {$_ eq $itemtype} @types;
503 $sum_checkouts->{total}++;
504 $sum_checkouts->{onsite_checkouts}++ if $c->onsite_checkout;
505 $sum_checkouts->{itemtype}->{$itemtype}++;
508 my $checkout_count_type = $sum_checkouts->{itemtype}->{$type} || 0;
509 my $checkout_count = $sum_checkouts->{total} || 0;
510 my $onsite_checkout_count = $sum_checkouts->{onsite_checkouts} || 0;
512 my $checkout_rules = {
513 checkout_count => $checkout_count,
514 onsite_checkout_count => $onsite_checkout_count,
515 onsite_checkout => $onsite_checkout,
516 max_checkouts_allowed => $maxissueqty_rule ? $maxissueqty_rule->rule_value : undef,
517 max_onsite_checkouts_allowed => $maxonsiteissueqty_rule ? $maxonsiteissueqty_rule->rule_value : undef,
518 switch_onsite_checkout => $switch_onsite_checkout,
520 # If parent rules exists
521 if ( defined($parent_maxissueqty_rule) and defined($parent_maxissueqty_rule->rule_value) ){
522 $checkout_rules->{max_checkouts_allowed} = $parent_maxissueqty_rule ? $parent_maxissueqty_rule->rule_value : undef;
523 my $qty_over = _check_max_qty($checkout_rules);
524 return $qty_over if defined $qty_over;
526 # If the parent rule is less than or equal to the child, we only need check the parent
527 if( $maxissueqty_rule->rule_value < $parent_maxissueqty_rule->rule_value && defined($maxissueqty_rule->itemtype) ) {
528 $checkout_rules->{checkout_count} = $checkout_count_type;
529 $checkout_rules->{max_checkouts_allowed} = $maxissueqty_rule ? $maxissueqty_rule->rule_value : undef;
530 my $qty_over = _check_max_qty($checkout_rules);
531 return $qty_over if defined $qty_over;
534 my $qty_over = _check_max_qty($checkout_rules);
535 return $qty_over if defined $qty_over;
539 # Now count total loans against the limit for the branch
540 my $branch_borrower_circ_rule = GetBranchBorrowerCircRule($branch, $cat_borrower);
541 if (defined($branch_borrower_circ_rule->{patron_maxissueqty}) and $branch_borrower_circ_rule->{patron_maxissueqty} ne '') {
543 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' ) {
544 $checkouts = $patron->checkouts->search(
545 { 'me.branchcode' => $branch} );
546 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
547 $checkouts = $patron->checkouts; # if branch is the patron's home branch, then count all loans by patron
549 $checkouts = $patron->checkouts->search(
550 { 'item.homebranch' => $branch},
551 { prefetch => 'item' } );
554 my $checkout_count = $checkouts->count;
555 my $onsite_checkout_count = $checkouts->search({ onsite_checkout => 1 })->count;
556 my $max_checkouts_allowed = $branch_borrower_circ_rule->{patron_maxissueqty};
557 my $max_onsite_checkouts_allowed = $branch_borrower_circ_rule->{patron_maxonsiteissueqty} || undef;
559 my $qty_over = _check_max_qty(
561 checkout_count => $checkout_count,
562 onsite_checkout_count => $onsite_checkout_count,
563 onsite_checkout => $onsite_checkout,
564 max_checkouts_allowed => $max_checkouts_allowed,
565 max_onsite_checkouts_allowed => $max_onsite_checkouts_allowed,
566 switch_onsite_checkout => $switch_onsite_checkout
569 return $qty_over if defined $qty_over;
572 if ( not defined( $maxissueqty_rule ) and not defined($branch_borrower_circ_rule->{patron_maxissueqty}) ) {
573 return { reason => 'NO_RULE_DEFINED', max_allowed => 0 };
576 # OK, the patron can issue !!!
582 my $checkout_count = $params->{checkout_count};
583 my $onsite_checkout_count = $params->{onsite_checkout_count};
584 my $onsite_checkout = $params->{onsite_checkout};
585 my $max_checkouts_allowed = $params->{max_checkouts_allowed};
586 my $max_onsite_checkouts_allowed = $params->{max_onsite_checkouts_allowed};
587 my $switch_onsite_checkout = $params->{switch_onsite_checkout};
589 if ( $onsite_checkout and defined $max_onsite_checkouts_allowed ) {
590 if ( $max_onsite_checkouts_allowed eq '' ) { return; }
591 if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed ) {
593 reason => 'TOO_MANY_ONSITE_CHECKOUTS',
594 count => $onsite_checkout_count,
595 max_allowed => $max_onsite_checkouts_allowed,
599 if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) {
600 if ( $max_checkouts_allowed eq '' ) { return; }
601 my $delta = $switch_onsite_checkout ? 1 : 0;
602 if ( $checkout_count >= $max_checkouts_allowed + $delta ) {
604 reason => 'TOO_MANY_CHECKOUTS',
605 count => $checkout_count,
606 max_allowed => $max_checkouts_allowed,
610 elsif ( not $onsite_checkout ) {
611 if ( $max_checkouts_allowed eq '' ) { return; }
613 $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed )
616 reason => 'TOO_MANY_CHECKOUTS',
617 count => $checkout_count - $onsite_checkout_count,
618 max_allowed => $max_checkouts_allowed,
626 =head2 CanBookBeIssued
628 ( $issuingimpossible, $needsconfirmation, [ $alerts ] ) = CanBookBeIssued( $patron,
629 $barcode, $duedate, $inprocess, $ignore_reserves, $params );
631 Check if a book can be issued.
633 C<$issuingimpossible> and C<$needsconfirmation> are hashrefs.
635 IMPORTANT: The assumption by users of this routine is that causes blocking
636 the issue are keyed by uppercase labels and other returned
637 data is keyed in lower case!
641 =item C<$patron> is a Koha::Patron
643 =item C<$barcode> is the bar code of the book being issued.
645 =item C<$duedates> is a DateTime object.
647 =item C<$inprocess> boolean switch
649 =item C<$ignore_reserves> boolean switch
651 =item C<$params> Hashref of additional parameters
654 override_high_holds - Ignore high holds
655 onsite_checkout - Checkout is an onsite checkout that will not leave the library
663 =item C<$issuingimpossible> a reference to a hash. It contains reasons why issuing is impossible.
664 Possible values are :
670 sticky due date is invalid
674 borrower gone with no address
678 borrower declared it's card lost
684 =head3 UNKNOWN_BARCODE
698 item is restricted (set by ??)
700 C<$needsconfirmation> a reference to a hash. It contains reasons why the loan
701 could be prevented, but ones that can be overriden by the operator.
703 Possible values are :
711 renewing, not issuing
713 =head3 ISSUED_TO_ANOTHER
715 issued to someone else.
719 reserved for someone else.
723 sticky due date is invalid or due date in the past
727 if the borrower borrows to much things
731 sub CanBookBeIssued {
732 my ( $patron, $barcode, $duedate, $inprocess, $ignore_reserves, $params ) = @_;
733 my %needsconfirmation; # filled with problems that needs confirmations
734 my %issuingimpossible; # filled with problems that causes the issue to be IMPOSSIBLE
735 my %alerts; # filled with messages that shouldn't stop issuing, but the librarian should be aware of.
736 my %messages; # filled with information messages that should be displayed.
738 my $onsite_checkout = $params->{onsite_checkout} || 0;
739 my $override_high_holds = $params->{override_high_holds} || 0;
741 my $item_object = Koha::Items->find({barcode => $barcode });
743 # MANDATORY CHECKS - unless item exists, nothing else matters
744 unless ( $item_object ) {
745 $issuingimpossible{UNKNOWN_BARCODE} = 1;
747 return ( \%issuingimpossible, \%needsconfirmation ) if %issuingimpossible;
749 my $item_unblessed = $item_object->unblessed; # Transition...
750 my $issue = $item_object->checkout;
751 my $biblio = $item_object->biblio;
753 my $biblioitem = $biblio->biblioitem;
754 my $effective_itemtype = $item_object->effective_itemtype;
755 my $dbh = C4::Context->dbh;
756 my $patron_unblessed = $patron->unblessed;
758 my $circ_library = Koha::Libraries->find( _GetCircControlBranch($item_unblessed, $patron_unblessed) );
760 # DUE DATE is OK ? -- should already have checked.
762 if ($duedate && ref $duedate ne 'DateTime') {
763 $duedate = dt_from_string($duedate);
765 my $now = dt_from_string();
766 unless ( $duedate ) {
767 my $issuedate = $now->clone();
769 $duedate = CalcDateDue( $issuedate, $effective_itemtype, $circ_library->branchcode, $patron_unblessed );
771 # Offline circ calls AddIssue directly, doesn't run through here
772 # So issuingimpossible should be ok.
775 my $fees = Koha::Charges::Fees->new(
778 library => $circ_library,
779 item => $item_object,
785 my $today = $now->clone();
786 $today->truncate( to => 'minute');
787 if (DateTime->compare($duedate,$today) == -1 ) { # duedate cannot be before now
788 $needsconfirmation{INVALID_DATE} = output_pref($duedate);
791 $issuingimpossible{INVALID_DATE} = output_pref($duedate);
797 if ( $patron->category->category_type eq 'X' && ( $item_object->barcode )) {
798 # stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1 .
800 branch => C4::Context->userenv->{'branch'},
802 itemnumber => $item_object->itemnumber,
803 itemtype => $effective_itemtype,
804 borrowernumber => $patron->borrowernumber,
805 ccode => $item_object->ccode}
807 ModDateLastSeen( $item_object->itemnumber ); # FIXME Move to Koha::Item
808 return( { STATS => 1 }, {});
811 if ( $patron->gonenoaddress && $patron->gonenoaddress == 1 ) {
812 $issuingimpossible{GNA} = 1;
815 if ( $patron->lost && $patron->lost == 1 ) {
816 $issuingimpossible{CARD_LOST} = 1;
818 if ( $patron->is_debarred ) {
819 $issuingimpossible{DEBARRED} = 1;
822 if ( $patron->is_expired ) {
823 $issuingimpossible{EXPIRED} = 1;
831 my $account = $patron->account;
832 my $balance = $account->balance;
833 my $non_issues_charges = $account->non_issues_charges;
834 my $other_charges = $balance - $non_issues_charges;
836 my $amountlimit = C4::Context->preference("noissuescharge");
837 my $allowfineoverride = C4::Context->preference("AllowFineOverride");
838 my $allfinesneedoverride = C4::Context->preference("AllFinesNeedOverride");
840 # Check the debt of this patrons guarantees
841 my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
842 $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
843 if ( defined $no_issues_charge_guarantees ) {
844 my @guarantees = map { $_->guarantee } $patron->guarantee_relationships();
845 my $guarantees_non_issues_charges = 0;
846 foreach my $g ( @guarantees ) {
847 $guarantees_non_issues_charges += $g->account->non_issues_charges;
850 if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && !$allowfineoverride) {
851 $issuingimpossible{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
852 } elsif ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && $allowfineoverride) {
853 $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
854 } elsif ( $allfinesneedoverride && $guarantees_non_issues_charges > 0 && $guarantees_non_issues_charges <= $no_issues_charge_guarantees && !$inprocess ) {
855 $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
859 # Check the debt of this patrons guarantors *and* the guarantees of those guarantors
860 my $no_issues_charge_guarantors = C4::Context->preference("NoIssuesChargeGuarantorsWithGuarantees");
861 $no_issues_charge_guarantors = undef unless looks_like_number( $no_issues_charge_guarantors );
862 if ( defined $no_issues_charge_guarantors ) {
863 my $guarantors_non_issues_charges += $patron->relationships_debt({ include_guarantors => 1, only_this_guarantor => 0, include_this_patron => 1 });
865 if ( $guarantors_non_issues_charges > $no_issues_charge_guarantors && !$inprocess && !$allowfineoverride) {
866 $issuingimpossible{DEBT_GUARANTORS} = $guarantors_non_issues_charges;
867 } elsif ( $guarantors_non_issues_charges > $no_issues_charge_guarantors && !$inprocess && $allowfineoverride) {
868 $needsconfirmation{DEBT_GUARANTORS} = $guarantors_non_issues_charges;
869 } elsif ( $allfinesneedoverride && $guarantors_non_issues_charges > 0 && $guarantors_non_issues_charges <= $no_issues_charge_guarantors && !$inprocess ) {
870 $needsconfirmation{DEBT_GUARANTORS} = $guarantors_non_issues_charges;
874 if ( C4::Context->preference("IssuingInProcess") ) {
875 if ( $non_issues_charges > $amountlimit && !$inprocess && !$allowfineoverride) {
876 $issuingimpossible{DEBT} = $non_issues_charges;
877 } elsif ( $non_issues_charges > $amountlimit && !$inprocess && $allowfineoverride) {
878 $needsconfirmation{DEBT} = $non_issues_charges;
879 } elsif ( $allfinesneedoverride && $non_issues_charges > 0 && $non_issues_charges <= $amountlimit && !$inprocess ) {
880 $needsconfirmation{DEBT} = $non_issues_charges;
884 if ( $non_issues_charges > $amountlimit && $allowfineoverride ) {
885 $needsconfirmation{DEBT} = $non_issues_charges;
886 } elsif ( $non_issues_charges > $amountlimit && !$allowfineoverride) {
887 $issuingimpossible{DEBT} = $non_issues_charges;
888 } elsif ( $non_issues_charges > 0 && $allfinesneedoverride ) {
889 $needsconfirmation{DEBT} = $non_issues_charges;
893 if ($balance > 0 && $other_charges > 0) {
894 $alerts{OTHER_CHARGES} = sprintf( "%.2f", $other_charges );
897 $patron = Koha::Patrons->find( $patron->borrowernumber ); # FIXME Refetch just in case, to avoid regressions. But must not be needed
898 $patron_unblessed = $patron->unblessed;
900 if ( my $debarred_date = $patron->is_debarred ) {
901 # patron has accrued fine days or has a restriction. $count is a date
902 if ($debarred_date eq '9999-12-31') {
903 $issuingimpossible{USERBLOCKEDNOENDDATE} = $debarred_date;
906 $issuingimpossible{USERBLOCKEDWITHENDDATE} = $debarred_date;
908 } elsif ( my $num_overdues = $patron->has_overdues ) {
909 ## patron has outstanding overdue loans
910 if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){
911 $issuingimpossible{USERBLOCKEDOVERDUE} = $num_overdues;
913 elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){
914 $needsconfirmation{USERBLOCKEDOVERDUE} = $num_overdues;
918 # Additional Materials Check
919 if ( C4::Context->preference("CircConfirmItemParts")
920 && $item_object->materials )
922 $needsconfirmation{ADDITIONAL_MATERIALS} = $item_object->materials;
926 # CHECK IF BOOK ALREADY ISSUED TO THIS BORROWER
928 if ( $issue && $issue->borrowernumber eq $patron->borrowernumber ){
930 # Already issued to current borrower.
931 # If it is an on-site checkout if it can be switched to a normal checkout
932 # or ask whether the loan should be renewed
934 if ( $issue->onsite_checkout
935 and C4::Context->preference('SwitchOnSiteCheckouts') ) {
936 $messages{ONSITE_CHECKOUT_WILL_BE_SWITCHED} = 1;
938 my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed(
939 $patron->borrowernumber,
940 $item_object->itemnumber,
942 if ( $CanBookBeRenewed == 0 ) { # no more renewals allowed
943 if ( $renewerror eq 'onsite_checkout' ) {
944 $issuingimpossible{NO_RENEWAL_FOR_ONSITE_CHECKOUTS} = 1;
947 $issuingimpossible{NO_MORE_RENEWALS} = 1;
951 $needsconfirmation{RENEW_ISSUE} = 1;
957 # issued to someone else
959 my $patron = Koha::Patrons->find( $issue->borrowernumber );
961 my ( $can_be_returned, $message ) = CanBookBeReturned( $item_unblessed, C4::Context->userenv->{branch} );
963 unless ( $can_be_returned ) {
964 $issuingimpossible{RETURN_IMPOSSIBLE} = 1;
965 $issuingimpossible{branch_to_return} = $message;
967 if ( C4::Context->preference('AutoReturnCheckedOutItems') ) {
968 $alerts{RETURNED_FROM_ANOTHER} = { patron => $patron };
970 $needsconfirmation{ISSUED_TO_ANOTHER} = 1;
971 $needsconfirmation{issued_firstname} = $patron->firstname;
972 $needsconfirmation{issued_surname} = $patron->surname;
973 $needsconfirmation{issued_cardnumber} = $patron->cardnumber;
974 $needsconfirmation{issued_borrowernumber} = $patron->borrowernumber;
979 # JB34 CHECKS IF BORROWERS DON'T HAVE ISSUE TOO MANY BOOKS
981 my $switch_onsite_checkout = (
982 C4::Context->preference('SwitchOnSiteCheckouts')
984 and $issue->onsite_checkout
985 and $issue->borrowernumber == $patron->borrowernumber ? 1 : 0 );
986 my $toomany = TooMany( $patron_unblessed, $item_object, { onsite_checkout => $onsite_checkout, switch_onsite_checkout => $switch_onsite_checkout, } );
987 # if TooMany max_allowed returns 0 the user doesn't have permission to check out this book
988 if ( $toomany && not exists $needsconfirmation{RENEW_ISSUE} ) {
989 if ( $toomany->{max_allowed} == 0 ) {
990 $needsconfirmation{PATRON_CANT} = 1;
992 if ( C4::Context->preference("AllowTooManyOverride") ) {
993 $needsconfirmation{TOO_MANY} = $toomany->{reason};
994 $needsconfirmation{current_loan_count} = $toomany->{count};
995 $needsconfirmation{max_loans_allowed} = $toomany->{max_allowed};
997 $issuingimpossible{TOO_MANY} = $toomany->{reason};
998 $issuingimpossible{current_loan_count} = $toomany->{count};
999 $issuingimpossible{max_loans_allowed} = $toomany->{max_allowed};
1004 # CHECKPREVCHECKOUT: CHECK IF ITEM HAS EVER BEEN LENT TO PATRON
1006 $patron = Koha::Patrons->find( $patron->borrowernumber ); # FIXME Refetch just in case, to avoid regressions. But must not be needed
1007 my $wants_check = $patron->wants_check_for_previous_checkout;
1008 $needsconfirmation{PREVISSUE} = 1
1009 if ($wants_check and $patron->do_check_for_previous_checkout($item_unblessed));
1014 if ( $item_object->notforloan )
1016 if(!C4::Context->preference("AllowNotForLoanOverride")){
1017 $issuingimpossible{NOT_FOR_LOAN} = 1;
1018 $issuingimpossible{item_notforloan} = $item_object->notforloan;
1020 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
1021 $needsconfirmation{item_notforloan} = $item_object->notforloan;
1025 # we have to check itemtypes.notforloan also
1026 if (C4::Context->preference('item-level_itypes')){
1027 # this should probably be a subroutine
1028 my $sth = $dbh->prepare("SELECT notforloan FROM itemtypes WHERE itemtype = ?");
1029 $sth->execute($effective_itemtype);
1030 my $notforloan=$sth->fetchrow_hashref();
1031 if ($notforloan->{'notforloan'}) {
1032 if (!C4::Context->preference("AllowNotForLoanOverride")) {
1033 $issuingimpossible{NOT_FOR_LOAN} = 1;
1034 $issuingimpossible{itemtype_notforloan} = $effective_itemtype;
1036 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
1037 $needsconfirmation{itemtype_notforloan} = $effective_itemtype;
1042 my $itemtype = Koha::ItemTypes->find($biblioitem->itemtype);
1043 if ( $itemtype && defined $itemtype->notforloan && $itemtype->notforloan == 1){
1044 if (!C4::Context->preference("AllowNotForLoanOverride")) {
1045 $issuingimpossible{NOT_FOR_LOAN} = 1;
1046 $issuingimpossible{itemtype_notforloan} = $effective_itemtype;
1048 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
1049 $needsconfirmation{itemtype_notforloan} = $effective_itemtype;
1054 if ( $item_object->withdrawn && $item_object->withdrawn > 0 )
1056 $issuingimpossible{WTHDRAWN} = 1;
1058 if ( $item_object->restricted
1059 && $item_object->restricted == 1 )
1061 $issuingimpossible{RESTRICTED} = 1;
1063 if ( $item_object->itemlost && C4::Context->preference("IssueLostItem") ne 'nothing' ) {
1064 my $av = Koha::AuthorisedValues->search({ category => 'LOST', authorised_value => $item_object->itemlost });
1065 my $code = $av->count ? $av->next->lib : '';
1066 $needsconfirmation{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'confirm' );
1067 $alerts{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'alert' );
1069 if ( C4::Context->preference("IndependentBranches") ) {
1070 my $userenv = C4::Context->userenv;
1071 unless ( C4::Context->IsSuperLibrarian() ) {
1072 my $HomeOrHoldingBranch = C4::Context->preference("HomeOrHoldingBranch");
1073 if ( $item_object->$HomeOrHoldingBranch ne $userenv->{branch} ){
1074 $issuingimpossible{ITEMNOTSAMEBRANCH} = 1;
1075 $issuingimpossible{'itemhomebranch'} = $item_object->$HomeOrHoldingBranch;
1077 $needsconfirmation{BORRNOTSAMEBRANCH} = $patron->branchcode
1078 if ( $patron->branchcode ne $userenv->{branch} );
1083 # CHECK IF THERE IS RENTAL CHARGES. RENTAL MUST BE CONFIRMED BY THE BORROWER
1085 my $rentalConfirmation = C4::Context->preference("RentalFeesCheckoutConfirmation");
1086 if ($rentalConfirmation) {
1087 my ($rentalCharge) = GetIssuingCharges( $item_object->itemnumber, $patron->borrowernumber );
1089 my $itemtype_object = Koha::ItemTypes->find( $item_object->effective_itemtype );
1090 if ($itemtype_object) {
1091 my $accumulate_charge = $fees->accumulate_rentalcharge();
1092 if ( $accumulate_charge > 0 ) {
1093 $rentalCharge += $accumulate_charge;
1097 if ( $rentalCharge > 0 ) {
1098 $needsconfirmation{RENTALCHARGE} = $rentalCharge;
1102 unless ( $ignore_reserves ) {
1103 # See if the item is on reserve.
1104 my ( $restype, $res ) = C4::Reserves::CheckReserves( $item_object->itemnumber );
1106 my $resbor = $res->{'borrowernumber'};
1107 if ( $resbor ne $patron->borrowernumber ) {
1108 my $patron = Koha::Patrons->find( $resbor );
1109 if ( $restype eq "Waiting" )
1111 # The item is on reserve and waiting, but has been
1112 # reserved by some other patron.
1113 $needsconfirmation{RESERVE_WAITING} = 1;
1114 $needsconfirmation{'resfirstname'} = $patron->firstname;
1115 $needsconfirmation{'ressurname'} = $patron->surname;
1116 $needsconfirmation{'rescardnumber'} = $patron->cardnumber;
1117 $needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
1118 $needsconfirmation{'resbranchcode'} = $res->{branchcode};
1119 $needsconfirmation{'reswaitingdate'} = $res->{'waitingdate'};
1120 $needsconfirmation{'reserve_id'} = $res->{reserve_id};
1122 elsif ( $restype eq "Reserved" ) {
1123 # The item is on reserve for someone else.
1124 $needsconfirmation{RESERVED} = 1;
1125 $needsconfirmation{'resfirstname'} = $patron->firstname;
1126 $needsconfirmation{'ressurname'} = $patron->surname;
1127 $needsconfirmation{'rescardnumber'} = $patron->cardnumber;
1128 $needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
1129 $needsconfirmation{'resbranchcode'} = $patron->branchcode;
1130 $needsconfirmation{'resreservedate'} = $res->{reservedate};
1131 $needsconfirmation{'reserve_id'} = $res->{reserve_id};
1137 ## CHECK AGE RESTRICTION
1138 my $agerestriction = $biblioitem->agerestriction;
1139 my ($restriction_age, $daysToAgeRestriction) = GetAgeRestriction( $agerestriction, $patron->unblessed );
1140 if ( $daysToAgeRestriction && $daysToAgeRestriction > 0 ) {
1141 if ( C4::Context->preference('AgeRestrictionOverride') ) {
1142 $needsconfirmation{AGE_RESTRICTION} = "$agerestriction";
1145 $issuingimpossible{AGE_RESTRICTION} = "$agerestriction";
1149 ## check for high holds decreasing loan period
1150 if ( C4::Context->preference('decreaseLoanHighHolds') ) {
1151 my $check = checkHighHolds( $item_unblessed, $patron_unblessed );
1153 if ( $check->{exceeded} ) {
1154 if ($override_high_holds) {
1155 $alerts{HIGHHOLDS} = {
1156 num_holds => $check->{outstanding},
1157 duration => $check->{duration},
1158 returndate => output_pref( { dt => dt_from_string($check->{due_date}), dateformat => 'iso', timeformat => '24hr' }),
1162 $needsconfirmation{HIGHHOLDS} = {
1163 num_holds => $check->{outstanding},
1164 duration => $check->{duration},
1165 returndate => output_pref( { dt => dt_from_string($check->{due_date}), dateformat => 'iso', timeformat => '24hr' }),
1172 !C4::Context->preference('AllowMultipleIssuesOnABiblio') &&
1173 # don't do the multiple loans per bib check if we've
1174 # already determined that we've got a loan on the same item
1175 !$issuingimpossible{NO_MORE_RENEWALS} &&
1176 !$needsconfirmation{RENEW_ISSUE}
1178 # Check if borrower has already issued an item from the same biblio
1179 # Only if it's not a subscription
1180 my $biblionumber = $item_object->biblionumber;
1181 require C4::Serials;
1182 my $is_a_subscription = C4::Serials::CountSubscriptionFromBiblionumber($biblionumber);
1183 unless ($is_a_subscription) {
1184 # FIXME Should be $patron->checkouts($args);
1185 my $checkouts = Koha::Checkouts->search(
1187 borrowernumber => $patron->borrowernumber,
1188 biblionumber => $biblionumber,
1194 # if we get here, we don't already have a loan on this item,
1195 # so if there are any loans on this bib, ask for confirmation
1196 if ( $checkouts->count ) {
1197 $needsconfirmation{BIBLIO_ALREADY_ISSUED} = 1;
1202 return ( \%issuingimpossible, \%needsconfirmation, \%alerts, \%messages, );
1205 =head2 CanBookBeReturned
1207 ($returnallowed, $message) = CanBookBeReturned($item, $branch)
1209 Check whether the item can be returned to the provided branch
1213 =item C<$item> is a hash of item information as returned Koha::Items->find->unblessed (Temporary, should be a Koha::Item instead)
1215 =item C<$branch> is the branchcode where the return is taking place
1223 =item C<$returnallowed> is 0 or 1, corresponding to whether the return is allowed (1) or not (0)
1225 =item C<$message> is the branchcode where the item SHOULD be returned, if the return is not allowed
1231 sub CanBookBeReturned {
1232 my ($item, $branch) = @_;
1233 my $allowreturntobranch = C4::Context->preference("AllowReturnToBranch") || 'anywhere';
1235 # assume return is allowed to start
1239 # identify all cases where return is forbidden
1240 if ($allowreturntobranch eq 'homebranch' && $branch ne $item->{'homebranch'}) {
1242 $message = $item->{'homebranch'};
1243 } elsif ($allowreturntobranch eq 'holdingbranch' && $branch ne $item->{'holdingbranch'}) {
1245 $message = $item->{'holdingbranch'};
1246 } elsif ($allowreturntobranch eq 'homeorholdingbranch' && $branch ne $item->{'homebranch'} && $branch ne $item->{'holdingbranch'}) {
1248 $message = $item->{'homebranch'}; # FIXME: choice of homebranch is arbitrary
1251 return ($allowed, $message);
1254 =head2 CheckHighHolds
1256 used when syspref decreaseLoanHighHolds is active. Returns 1 or 0 to define whether the minimum value held in
1257 decreaseLoanHighHoldsValue is exceeded, the total number of outstanding holds, the number of days the loan
1258 has been decreased to (held in syspref decreaseLoanHighHoldsValue), and the new due date
1262 sub checkHighHolds {
1263 my ( $item, $borrower ) = @_;
1264 my $branchcode = _GetCircControlBranch( $item, $borrower );
1265 my $item_object = Koha::Items->find( $item->{itemnumber} );
1274 my $holds = Koha::Holds->search( { biblionumber => $item->{'biblionumber'} } );
1276 if ( $holds->count() ) {
1277 $return_data->{outstanding} = $holds->count();
1279 my $decreaseLoanHighHoldsControl = C4::Context->preference('decreaseLoanHighHoldsControl');
1280 my $decreaseLoanHighHoldsValue = C4::Context->preference('decreaseLoanHighHoldsValue');
1281 my $decreaseLoanHighHoldsIgnoreStatuses = C4::Context->preference('decreaseLoanHighHoldsIgnoreStatuses');
1283 my @decreaseLoanHighHoldsIgnoreStatuses = split( /,/, $decreaseLoanHighHoldsIgnoreStatuses );
1285 if ( $decreaseLoanHighHoldsControl eq 'static' ) {
1287 # static means just more than a given number of holds on the record
1289 # If the number of holds is less than the threshold, we can stop here
1290 if ( $holds->count() < $decreaseLoanHighHoldsValue ) {
1291 return $return_data;
1294 elsif ( $decreaseLoanHighHoldsControl eq 'dynamic' ) {
1296 # dynamic means X more than the number of holdable items on the record
1298 # let's get the items
1299 my @items = $holds->next()->biblio()->items()->as_list;
1301 # Remove any items with status defined to be ignored even if the would not make item unholdable
1302 foreach my $status (@decreaseLoanHighHoldsIgnoreStatuses) {
1303 @items = grep { !$_->$status } @items;
1306 # Remove any items that are not holdable for this patron
1307 @items = grep { CanItemBeReserved( $borrower->{borrowernumber}, $_->itemnumber, undef, { ignore_found_holds => 1 } )->{status} eq 'OK' } @items;
1309 my $items_count = scalar @items;
1311 my $threshold = $items_count + $decreaseLoanHighHoldsValue;
1313 # If the number of holds is less than the count of items we have
1314 # plus the number of holds allowed above that count, we can stop here
1315 if ( $holds->count() <= $threshold ) {
1316 return $return_data;
1320 my $issuedate = dt_from_string();
1322 my $itype = $item_object->effective_itemtype;
1323 my $daysmode = Koha::CirculationRules->get_effective_daysmode(
1325 categorycode => $borrower->{categorycode},
1327 branchcode => $branchcode,
1330 my $calendar = Koha::Calendar->new( branchcode => $branchcode, days_mode => $daysmode );
1332 my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branchcode, $borrower );
1334 my $rule = Koha::CirculationRules->get_effective_rule(
1336 categorycode => $borrower->{categorycode},
1337 itemtype => $item_object->effective_itemtype,
1338 branchcode => $branchcode,
1339 rule_name => 'decreaseloanholds',
1344 if ( defined($rule) && $rule->rule_value ne '' ){
1345 # overrides decreaseLoanHighHoldsDuration syspref
1346 $duration = $rule->rule_value;
1348 $duration = C4::Context->preference('decreaseLoanHighHoldsDuration');
1350 my $reduced_datedue = $calendar->addDuration( $issuedate, $duration );
1351 $reduced_datedue->set_hour($orig_due->hour);
1352 $reduced_datedue->set_minute($orig_due->minute);
1353 $reduced_datedue->truncate( to => 'minute' );
1355 if ( DateTime->compare( $reduced_datedue, $orig_due ) == -1 ) {
1356 $return_data->{exceeded} = 1;
1357 $return_data->{duration} = $duration;
1358 $return_data->{due_date} = $reduced_datedue;
1362 return $return_data;
1367 &AddIssue($borrower, $barcode, [$datedue], [$cancelreserve], [$issuedate])
1369 Issue a book. Does no check, they are done in CanBookBeIssued. If we reach this sub, it means the user confirmed if needed.
1373 =item C<$borrower> is a hash with borrower informations (from Koha::Patron->unblessed).
1375 =item C<$barcode> is the barcode of the item being issued.
1377 =item C<$datedue> is a DateTime object for the max date of return, i.e. the date due (optional).
1378 Calculated if empty.
1380 =item C<$cancelreserve> is 1 to override and cancel any pending reserves for the item (optional).
1382 =item C<$issuedate> is the date to issue the item in iso (YYYY-MM-DD) format (optional).
1383 Defaults to today. Unlike C<$datedue>, NOT a DateTime object, unfortunately.
1385 AddIssue does the following things :
1387 - step 01: check that there is a borrowernumber & a barcode provided
1388 - check for RENEWAL (book issued & being issued to the same patron)
1389 - renewal YES = Calculate Charge & renew
1391 * BOOK ACTUALLY ISSUED ? do a return if book is actually issued (but to someone else)
1393 - fill reserve if reserve to this patron
1394 - cancel reserve or not, otherwise
1395 * TRANSFERT PENDING ?
1396 - complete the transfert
1404 my ( $borrower, $barcode, $datedue, $cancelreserve, $issuedate, $sipmode, $params ) = @_;
1406 my $onsite_checkout = $params && $params->{onsite_checkout} ? 1 : 0;
1407 my $switch_onsite_checkout = $params && $params->{switch_onsite_checkout};
1408 my $auto_renew = $params && $params->{auto_renew};
1409 my $dbh = C4::Context->dbh;
1410 my $barcodecheck = CheckValidBarcode($barcode);
1414 if ( $datedue && ref $datedue ne 'DateTime' ) {
1415 $datedue = dt_from_string($datedue);
1418 # $issuedate defaults to today.
1419 if ( !defined $issuedate ) {
1420 $issuedate = dt_from_string();
1423 if ( ref $issuedate ne 'DateTime' ) {
1424 $issuedate = dt_from_string($issuedate);
1429 # Stop here if the patron or barcode doesn't exist
1430 if ( $borrower && $barcode && $barcodecheck ) {
1431 # find which item we issue
1432 my $item_object = Koha::Items->find({ barcode => $barcode })
1433 or return; # if we don't get an Item, abort.
1434 my $item_unblessed = $item_object->unblessed;
1436 my $branchcode = _GetCircControlBranch( $item_unblessed, $borrower );
1438 # get actual issuing if there is one
1439 my $actualissue = $item_object->checkout;
1441 # check if we just renew the issue.
1442 if ( $actualissue and $actualissue->borrowernumber eq $borrower->{'borrowernumber'}
1443 and not $switch_onsite_checkout ) {
1444 $datedue = AddRenewal(
1445 $borrower->{'borrowernumber'},
1446 $item_object->itemnumber,
1449 $issuedate, # here interpreted as the renewal date
1454 my $itype = $item_object->effective_itemtype;
1455 $datedue = CalcDateDue( $issuedate, $itype, $branchcode, $borrower );
1458 $datedue->truncate( to => 'minute' );
1460 my $patron = Koha::Patrons->find( $borrower );
1461 my $library = Koha::Libraries->find( $branchcode );
1462 my $fees = Koha::Charges::Fees->new(
1465 library => $library,
1466 item => $item_object,
1467 to_date => $datedue,
1471 # it's NOT a renewal
1472 if ( $actualissue and not $switch_onsite_checkout ) {
1473 # This book is currently on loan, but not to the person
1474 # who wants to borrow it now. mark it returned before issuing to the new borrower
1475 my ( $allowed, $message ) = CanBookBeReturned( $item_unblessed, C4::Context->userenv->{branch} );
1476 return unless $allowed;
1477 AddReturn( $item_object->barcode, C4::Context->userenv->{'branch'} );
1480 C4::Reserves::MoveReserve( $item_object->itemnumber, $borrower->{'borrowernumber'}, $cancelreserve );
1482 # Starting process for transfer job (checking transfert and validate it if we have one)
1483 if ( my $transfer = $item_object->get_transfer ) {
1484 # updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....)
1487 datearrived => dt_from_string,
1488 tobranch => C4::Context->userenv->{branch},
1489 comments => 'Forced branchtransfer'
1492 if ( $transfer->reason && $transfer->reason eq 'Reserve' ) {
1493 my $hold = $item_object->holds->search( { found => 'T' } )->next;
1494 if ( $hold ) { # Is this really needed?
1495 $hold->set( { found => undef } )->store;
1496 C4::Reserves::ModReserveMinusPriority($item_object->itemnumber, $hold->reserve_id);
1501 # If automatic renewal wasn't selected while issuing, set the value according to the issuing rule.
1502 unless ($auto_renew) {
1503 my $rule = Koha::CirculationRules->get_effective_rule(
1505 categorycode => $borrower->{categorycode},
1506 itemtype => $item_object->effective_itemtype,
1507 branchcode => $branchcode,
1508 rule_name => 'auto_renew'
1512 $auto_renew = $rule->rule_value if $rule;
1515 my $issue_attributes = {
1516 borrowernumber => $borrower->{'borrowernumber'},
1517 issuedate => $issuedate->strftime('%Y-%m-%d %H:%M:%S'),
1518 date_due => $datedue->strftime('%Y-%m-%d %H:%M:%S'),
1519 branchcode => C4::Context->userenv->{'branch'},
1520 onsite_checkout => $onsite_checkout,
1521 auto_renew => $auto_renew ? 1 : 0,
1524 # Get ID of logged in user. if called from a batch job,
1525 # no user session exists and C4::Context->userenv() returns
1526 # the scalar '0'. Only do this if the syspref says so
1527 if ( C4::Context->preference('RecordStaffUserOnCheckout') ) {
1528 my $userenv = C4::Context->userenv();
1529 my $usernumber = (ref($userenv) eq 'HASH') ? $userenv->{'number'} : undef;
1531 $issue_attributes->{issuer_id} = $usernumber;
1535 # In the case that the borrower has an on-site checkout
1536 # and SwitchOnSiteCheckouts is enabled this converts it to a regular checkout
1537 $issue = Koha::Checkouts->find( { itemnumber => $item_object->itemnumber } );
1539 $issue->set($issue_attributes)->store;
1542 $issue = Koha::Checkout->new(
1544 itemnumber => $item_object->itemnumber,
1549 $issue->discard_changes;
1550 if ( $item_object->location && $item_object->location eq 'CART'
1551 && ( !$item_object->permanent_location || $item_object->permanent_location ne 'CART' ) ) {
1552 ## Item was moved to cart via UpdateItemLocationOnCheckin, anything issued should be taken off the cart.
1553 CartToShelf( $item_object->itemnumber );
1556 if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) {
1557 UpdateTotalIssues( $item_object->biblionumber, 1 );
1560 # Record if item was lost
1561 my $was_lost = $item_object->itemlost;
1563 $item_object->issues( ( $item_object->issues || 0 ) + 1);
1564 $item_object->holdingbranch(C4::Context->userenv->{'branch'});
1565 $item_object->itemlost(0);
1566 $item_object->onloan($datedue->ymd());
1567 $item_object->datelastborrowed( dt_from_string()->ymd() );
1568 $item_object->datelastseen( dt_from_string()->ymd() );
1569 $item_object->store({log_action => 0});
1571 # If the item was lost, it has now been found, charge the overdue if necessary
1573 if ( $item_object->{_charge} ) {
1574 $actualissue //= Koha::Old::Checkouts->search(
1575 { itemnumber => $item_unblessed->{itemnumber} },
1577 order_by => { '-desc' => 'returndate' },
1581 unless ( exists( $borrower->{branchcode} ) ) {
1582 my $patron = $actualissue->patron;
1583 $borrower = $patron->unblessed;
1585 _CalculateAndUpdateFine(
1587 issue => $actualissue,
1588 item => $item_unblessed,
1589 borrower => $borrower,
1590 return_date => $issuedate
1593 _FixOverduesOnReturn( $borrower->{borrowernumber},
1594 $item_object->itemnumber, undef, 'RENEWED' );
1598 # If it costs to borrow this book, charge it to the patron's account.
1599 my ( $charge, $itemtype ) = GetIssuingCharges( $item_object->itemnumber, $borrower->{'borrowernumber'} );
1600 if ( $charge && $charge > 0 ) {
1601 AddIssuingCharge( $issue, $charge, 'RENT' );
1604 my $itemtype_object = Koha::ItemTypes->find( $item_object->effective_itemtype );
1605 if ( $itemtype_object ) {
1606 my $accumulate_charge = $fees->accumulate_rentalcharge();
1607 if ( $accumulate_charge > 0 ) {
1608 AddIssuingCharge( $issue, $accumulate_charge, 'RENT_DAILY' );
1609 $charge += $accumulate_charge;
1610 $item_unblessed->{charge} = $charge;
1614 # Record the fact that this book was issued.
1617 branch => C4::Context->userenv->{'branch'},
1618 type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
1620 other => ( $sipmode ? "SIP-$sipmode" : '' ),
1621 itemnumber => $item_object->itemnumber,
1622 itemtype => $item_object->effective_itemtype,
1623 location => $item_object->location,
1624 borrowernumber => $borrower->{'borrowernumber'},
1625 ccode => $item_object->ccode,
1629 # Send a checkout slip.
1630 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1632 branchcode => $branchcode,
1633 categorycode => $borrower->{categorycode},
1634 item_type => $item_object->effective_itemtype,
1635 notification => 'CHECKOUT',
1637 if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
1638 SendCirculationAlert(
1641 item => $item_object->unblessed,
1642 borrower => $borrower,
1643 branch => $branchcode,
1648 "CIRCULATION", "ISSUE",
1649 $borrower->{'borrowernumber'},
1650 $item_object->itemnumber,
1651 ) if C4::Context->preference("IssueLog");
1653 Koha::Plugins->call('after_circ_action', {
1654 action => 'checkout',
1656 type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
1657 checkout => $issue->get_from_storage
1665 =head2 GetLoanLength
1667 my $loanlength = &GetLoanLength($borrowertype,$itemtype,branchcode)
1669 Get loan length for an itemtype, a borrower type and a branch
1674 my ( $categorycode, $itemtype, $branchcode ) = @_;
1676 # Initialize default values
1680 lengthunit => 'days',
1683 my $found = Koha::CirculationRules->get_effective_rules( {
1684 branchcode => $branchcode,
1685 categorycode => $categorycode,
1686 itemtype => $itemtype,
1695 foreach my $rule_name (keys %$found) {
1696 $rules->{$rule_name} = $found->{$rule_name};
1703 =head2 GetHardDueDate
1705 my ($hardduedate,$hardduedatecompare) = &GetHardDueDate($borrowertype,$itemtype,branchcode)
1707 Get the Hard Due Date and it's comparison for an itemtype, a borrower type and a branch
1711 sub GetHardDueDate {
1712 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1714 my $rules = Koha::CirculationRules->get_effective_rules(
1716 categorycode => $borrowertype,
1717 itemtype => $itemtype,
1718 branchcode => $branchcode,
1719 rules => [ 'hardduedate', 'hardduedatecompare' ],
1723 if ( defined( $rules->{hardduedate} ) ) {
1724 if ( $rules->{hardduedate} ) {
1725 return ( dt_from_string( $rules->{hardduedate}, 'iso' ), $rules->{hardduedatecompare} );
1728 return ( undef, undef );
1733 =head2 GetBranchBorrowerCircRule
1735 my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
1737 Retrieves circulation rule attributes that apply to the given
1738 branch and patron category, regardless of item type.
1739 The return value is a hashref containing the following key:
1741 patron_maxissueqty - maximum number of loans that a
1742 patron of the given category can have at the given
1743 branch. If the value is undef, no limit.
1745 patron_maxonsiteissueqty - maximum of on-site checkouts that a
1746 patron of the given category can have at the given
1747 branch. If the value is undef, no limit.
1749 This will check for different branch/category combinations in the following order:
1753 default branch and category
1755 If no rule has been found in the database, it will default to
1758 patron_maxissueqty - undef
1759 patron_maxonsiteissueqty - undef
1761 C<$branchcode> and C<$categorycode> should contain the
1762 literal branch code and patron category code, respectively - no
1767 sub GetBranchBorrowerCircRule {
1768 my ( $branchcode, $categorycode ) = @_;
1770 # Initialize default values
1772 patron_maxissueqty => undef,
1773 patron_maxonsiteissueqty => undef,
1777 foreach my $rule_name (qw( patron_maxissueqty patron_maxonsiteissueqty )) {
1778 my $rule = Koha::CirculationRules->get_effective_rule(
1780 categorycode => $categorycode,
1782 branchcode => $branchcode,
1783 rule_name => $rule_name,
1787 $rules->{$rule_name} = $rule->rule_value if defined $rule;
1793 =head2 GetBranchItemRule
1795 my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
1797 Retrieves circulation rule attributes that apply to the given
1798 branch and item type, regardless of patron category.
1800 The return value is a hashref containing the following keys:
1802 holdallowed => Hold policy for this branch and itemtype. Possible values:
1803 0: No holds allowed.
1804 1: Holds allowed only by patrons that have the same homebranch as the item.
1805 2: Holds allowed from any patron.
1807 returnbranch => branch to which to return item. Possible values:
1808 noreturn: do not return, let item remain where checked in (floating collections)
1809 homebranch: return to item's home branch
1810 holdingbranch: return to issuer branch
1812 This searches branchitemrules in the following order:
1814 * Same branchcode and itemtype
1815 * Same branchcode, itemtype '*'
1816 * branchcode '*', same itemtype
1817 * branchcode and itemtype '*'
1819 Neither C<$branchcode> nor C<$itemtype> should be '*'.
1823 sub GetBranchItemRule {
1824 my ( $branchcode, $itemtype ) = @_;
1827 my $holdallowed_rule = Koha::CirculationRules->get_effective_rule(
1829 branchcode => $branchcode,
1830 itemtype => $itemtype,
1831 rule_name => 'holdallowed',
1834 my $hold_fulfillment_policy_rule = Koha::CirculationRules->get_effective_rule(
1836 branchcode => $branchcode,
1837 itemtype => $itemtype,
1838 rule_name => 'hold_fulfillment_policy',
1841 my $returnbranch_rule = Koha::CirculationRules->get_effective_rule(
1843 branchcode => $branchcode,
1844 itemtype => $itemtype,
1845 rule_name => 'returnbranch',
1849 # built-in default circulation rule
1851 $rules->{holdallowed} = defined $holdallowed_rule
1852 ? $holdallowed_rule->rule_value
1854 $rules->{hold_fulfillment_policy} = defined $hold_fulfillment_policy_rule
1855 ? $hold_fulfillment_policy_rule->rule_value
1857 $rules->{returnbranch} = defined $returnbranch_rule
1858 ? $returnbranch_rule->rule_value
1866 ($doreturn, $messages, $iteminformation, $borrower) =
1867 &AddReturn( $barcode, $branch [,$exemptfine] [,$returndate] );
1873 =item C<$barcode> is the bar code of the book being returned.
1875 =item C<$branch> is the code of the branch where the book is being returned.
1877 =item C<$exemptfine> indicates that overdue charges for the item will be
1880 =item C<$return_date> allows the default return date to be overridden
1881 by the given return date. Optional.
1885 C<&AddReturn> returns a list of four items:
1887 C<$doreturn> is true iff the return succeeded.
1889 C<$messages> is a reference-to-hash giving feedback on the operation.
1890 The keys of the hash are:
1896 No item with this barcode exists. The value is C<$barcode>.
1900 The book is not currently on loan. The value is C<$barcode>.
1904 This book has been withdrawn/cancelled. The value should be ignored.
1906 =item C<Wrongbranch>
1908 This book has was returned to the wrong branch. The value is a hashref
1909 so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
1910 contain the branchcode of the incorrect and correct return library, respectively.
1914 The item was reserved. The value is a reference-to-hash whose keys are
1915 fields from the reserves table of the Koha database, and
1916 C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
1917 either C<Waiting>, C<Reserved>, or 0.
1919 =item C<WasReturned>
1921 Value 1 if return is successful.
1923 =item C<NeedsTransfer>
1925 If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer.
1929 C<$iteminformation> is a reference-to-hash, giving information about the
1930 returned item from the issues table.
1932 C<$borrower> is a reference-to-hash, giving information about the
1933 patron who last borrowed the book.
1938 my ( $barcode, $branch, $exemptfine, $return_date ) = @_;
1940 if ($branch and not Koha::Libraries->find($branch)) {
1941 warn "AddReturn error: branch '$branch' not found. Reverting to " . C4::Context->userenv->{'branch'};
1944 $branch = C4::Context->userenv->{'branch'} unless $branch; # we trust userenv to be a safe fallback/default
1945 my $return_date_specified = !!$return_date;
1946 $return_date //= dt_from_string();
1950 my $validTransfer = 1;
1951 my $stat_type = 'return';
1953 # get information on item
1954 my $item = Koha::Items->find({ barcode => $barcode });
1956 return ( 0, { BadBarcode => $barcode } ); # no barcode means no item or borrower. bail out.
1959 my $itemnumber = $item->itemnumber;
1960 my $itemtype = $item->effective_itemtype;
1962 my $issue = $item->checkout;
1964 $patron = $issue->patron
1965 or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existent borrowernumber '" . $issue->borrowernumber . "'\n"
1966 . Dumper($issue->unblessed) . "\n";
1968 $messages->{'NotIssued'} = $barcode;
1969 $item->onloan(undef)->store({skip_record_index=>1}) if defined $item->onloan;
1971 # even though item is not on loan, it may still be transferred; therefore, get current branch info
1973 # No issue, no borrowernumber. ONLY if $doreturn, *might* you have a $borrower later.
1974 # Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
1975 if (C4::Context->preference("RecordLocalUseOnReturn")) {
1976 $messages->{'LocalUse'} = 1;
1977 $stat_type = 'localuse';
1981 # full item data, but no borrowernumber or checkout info (no issue)
1982 my $hbr = GetBranchItemRule($item->homebranch, $itemtype)->{'returnbranch'} || "homebranch";
1983 # get the proper branch to which to return the item
1984 my $returnbranch = $hbr ne 'noreturn' ? $item->$hbr : $branch;
1985 # if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
1986 my $transfer_trigger = $hbr eq 'homebranch' ? 'ReturnToHome' : $hbr eq 'holdingbranch' ? 'ReturnToHolding' : undef;
1988 my $borrowernumber = $patron ? $patron->borrowernumber : undef; # we don't know if we had a borrower or not
1989 my $patron_unblessed = $patron ? $patron->unblessed : {};
1991 my $update_loc_rules = Koha::Config::SysPrefs->find('UpdateItemLocationOnCheckin')->get_yaml_pref_hash();
1992 map { $update_loc_rules->{$_} = $update_loc_rules->{$_}[0] } keys %$update_loc_rules; #We can only move to one location so we flatten the arrays
1993 if ($update_loc_rules) {
1994 if (defined $update_loc_rules->{_ALL_}) {
1995 if ($update_loc_rules->{_ALL_} eq '_PERM_') { $update_loc_rules->{_ALL_} = $item->permanent_location; }
1996 if ($update_loc_rules->{_ALL_} eq '_BLANK_') { $update_loc_rules->{_ALL_} = ''; }
1997 if ( defined $item->location && $item->location ne $update_loc_rules->{_ALL_}) {
1998 $messages->{'ItemLocationUpdated'} = { from => $item->location, to => $update_loc_rules->{_ALL_} };
1999 $item->location($update_loc_rules->{_ALL_})->store({skip_record_index=>1});
2003 foreach my $key ( keys %$update_loc_rules ) {
2004 if ( $update_loc_rules->{$key} eq '_PERM_' ) { $update_loc_rules->{$key} = $item->permanent_location; }
2005 if ( $update_loc_rules->{$key} eq '_BLANK_') { $update_loc_rules->{$key} = '' ;}
2006 if ( ($item->location eq $key && $item->location ne $update_loc_rules->{$key}) || ($key eq '_BLANK_' && $item->location eq '' && $update_loc_rules->{$key} ne '') ) {
2007 $messages->{'ItemLocationUpdated'} = { from => $item->location, to => $update_loc_rules->{$key} };
2008 $item->location($update_loc_rules->{$key})->store({skip_record_index=>1});
2015 my $yaml = C4::Context->preference('UpdateNotForLoanStatusOnCheckin');
2017 $yaml = "$yaml\n\n"; # YAML is anal on ending \n. Surplus does not hurt
2019 eval { $rules = YAML::XS::Load($yaml); };
2021 warn "Unable to parse UpdateNotForLoanStatusOnCheckin syspref : $@";
2024 foreach my $key ( keys %$rules ) {
2025 if ( $item->notforloan eq $key ) {
2026 $messages->{'NotForLoanStatusUpdated'} = { from => $item->notforloan, to => $rules->{$key} };
2027 $item->notforloan($rules->{$key})->store({ log_action => 0, skip_record_index => 1 });
2034 # check if the return is allowed at this branch
2035 my ($returnallowed, $message) = CanBookBeReturned($item->unblessed, $branch);
2036 unless ($returnallowed){
2037 $messages->{'Wrongbranch'} = {
2038 Wrongbranch => $branch,
2039 Rightbranch => $message
2042 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
2043 $indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
2044 return ( $doreturn, $messages, $issue, $patron_unblessed);
2047 if ( $item->withdrawn ) { # book has been cancelled
2048 $messages->{'withdrawn'} = 1;
2049 $doreturn = 0 if C4::Context->preference("BlockReturnOfWithdrawnItems");
2052 if ( $item->itemlost and C4::Context->preference("BlockReturnOfLostItems") ) {
2056 # case of a return of document (deal with issues and holdingbranch)
2058 die "The item is not issed and cannot be returned" unless $issue; # Just in case...
2059 $patron or warn "AddReturn without current borrower";
2063 MarkIssueReturned( $borrowernumber, $item->itemnumber, $return_date, $patron->privacy, { skip_record_index => 1} );
2068 C4::Context->preference('CalculateFinesOnReturn')
2069 || ( $return_date_specified && C4::Context->preference('CalculateFinesOnBackdate') )
2074 _CalculateAndUpdateFine( { issue => $issue, item => $item->unblessed, borrower => $patron_unblessed, return_date => $return_date } );
2077 carp "The checkin for the following issue failed, Please go to the about page and check all messages on the 'System information' to see if there are configuration / data issues ($@)" . Dumper( $issue->unblessed );
2079 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
2080 $indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
2082 return ( 0, { WasReturned => 0, DataCorrupted => 1 }, $issue, $patron_unblessed );
2085 # FIXME is the "= 1" right? This could be the borrower hash.
2086 $messages->{'WasReturned'} = 1;
2089 $item->onloan(undef)->store({ log_action => 0 , skip_record_index => 1 });
2093 # the holdingbranch is updated if the document is returned to another location.
2094 # this is always done regardless of whether the item was on loan or not
2095 if ($item->holdingbranch ne $branch) {
2096 $item->holdingbranch($branch)->store({ skip_record_index => 1 });
2099 my $item_was_lost = $item->itemlost;
2100 my $leave_item_lost = C4::Context->preference("BlockReturnOfLostItems") ? 1 : 0;
2101 my $updated_item = ModDateLastSeen( $item->itemnumber, $leave_item_lost, { skip_record_index => 1 } ); # will unset itemlost if needed
2103 # fix up the accounts.....
2104 if ($item_was_lost) {
2105 $messages->{'WasLost'} = 1;
2106 unless ( C4::Context->preference("BlockReturnOfLostItems") ) {
2107 $messages->{'LostItemFeeRefunded'} = $updated_item->{_refunded};
2108 $messages->{'LostItemFeeRestored'} = $updated_item->{_restored};
2110 if ( $updated_item->{_charge} ) {
2111 $issue //= Koha::Old::Checkouts->search(
2112 { itemnumber => $item->itemnumber },
2113 { order_by => { '-desc' => 'returndate' }, rows => 1 } )
2115 unless ( exists( $patron_unblessed->{branchcode} ) ) {
2116 my $patron = $issue->patron;
2117 $patron_unblessed = $patron->unblessed;
2119 _CalculateAndUpdateFine(
2122 item => $item->unblessed,
2123 borrower => $patron_unblessed,
2124 return_date => $return_date
2127 _FixOverduesOnReturn( $patron_unblessed->{borrowernumber},
2128 $item->itemnumber, undef, 'RETURNED' );
2129 $messages->{'LostItemFeeCharged'} = 1;
2134 # check if we have a transfer for this document
2135 my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->itemnumber );
2137 # if we have a transfer to complete, we update the line of transfers with the datearrived
2138 my $is_in_rotating_collection = C4::RotatingCollections::isItemInAnyCollection( $item->itemnumber );
2140 # At this point we will either fill the transfer or it is a wrong transfer
2141 # either way we should not now generate a new transfer
2143 if ( $tobranch eq $branch ) {
2144 my $sth = C4::Context->dbh->prepare(
2145 "UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL"
2147 $sth->execute( $item->itemnumber );
2148 $messages->{'TransferArrived'} = $frombranch;
2150 $messages->{'WrongTransfer'} = $tobranch;
2151 $messages->{'WrongTransferItem'} = $item->itemnumber;
2155 # fix up the overdues in accounts...
2156 if ($borrowernumber) {
2157 my $fix = _FixOverduesOnReturn( $borrowernumber, $item->itemnumber, $exemptfine, 'RETURNED' );
2158 defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, ".$item->itemnumber."...) failed!"; # zero is OK, check defined
2160 if ( $issue and $issue->is_overdue($return_date) ) {
2162 my ($debardate,$reminder) = _debar_user_on_return( $patron_unblessed, $item->unblessed, dt_from_string($issue->date_due), $return_date );
2164 $messages->{'PrevDebarred'} = $debardate;
2166 $messages->{'Debarred'} = $debardate if $debardate;
2168 # there's no overdue on the item but borrower had been previously debarred
2169 } elsif ( $issue->date_due and $patron->debarred ) {
2170 if ( $patron->debarred eq "9999-12-31") {
2171 $messages->{'ForeverDebarred'} = $patron->debarred;
2173 my $borrower_debar_dt = dt_from_string( $patron->debarred );
2174 $borrower_debar_dt->truncate(to => 'day');
2175 my $today_dt = $return_date->clone()->truncate(to => 'day');
2176 if ( DateTime->compare( $borrower_debar_dt, $today_dt ) != -1 ) {
2177 $messages->{'PrevDebarred'} = $patron->debarred;
2183 # find reserves.....
2184 # launch the Checkreserves routine to find any holds
2185 my ($resfound, $resrec);
2186 my $lookahead= C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
2187 ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->itemnumber, undef, $lookahead ) unless ( $item->withdrawn );
2188 # if a hold is found and is waiting at another branch, change the priority back to 1 and trigger the hold (this will trigger a transfer and update the hold status properly)
2189 if ( $resfound and $resfound eq "Waiting" and $branch ne $resrec->{branchcode} ) {
2190 my $hold = C4::Reserves::RevertWaitingStatus( { itemnumber => $item->itemnumber } );
2191 $resfound = 'Reserved';
2192 $resrec = $hold->unblessed;
2195 $resrec->{'ResFound'} = $resfound;
2196 $messages->{'ResFound'} = $resrec;
2199 # Record the fact that this book was returned.
2203 itemnumber => $itemnumber,
2204 itemtype => $itemtype,
2205 location => $item->location,
2206 borrowernumber => $borrowernumber,
2207 ccode => $item->ccode,
2210 # Send a check-in slip. # NOTE: borrower may be undef. Do not try to send messages then.
2212 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2214 branchcode => $branch,
2215 categorycode => $patron->categorycode,
2216 item_type => $itemtype,
2217 notification => 'CHECKIN',
2219 if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
2220 SendCirculationAlert({
2222 item => $item->unblessed,
2223 borrower => $patron->unblessed,
2228 logaction("CIRCULATION", "RETURN", $borrowernumber, $item->itemnumber)
2229 if C4::Context->preference("ReturnLog");
2232 # Check if this item belongs to a biblio record that is attached to an
2233 # ILL request, if it is we need to update the ILL request's status
2234 if ( $doreturn and C4::Context->preference('CirculateILL')) {
2235 my $request = Koha::Illrequests->find(
2236 { biblio_id => $item->biblio->biblionumber }
2238 $request->status('RET') if $request;
2241 # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
2242 if ($validTransfer && !$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) ){
2243 my $BranchTransferLimitsType = C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ? 'effective_itemtype' : 'ccode';
2244 if (C4::Context->preference("AutomaticItemReturn" ) or
2245 (C4::Context->preference("UseBranchTransferLimits") and
2246 ! IsBranchTransferAllowed($branch, $returnbranch, $item->$BranchTransferLimitsType )
2248 $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s, %s)", $item->itemnumber,$branch, $returnbranch, $transfer_trigger;
2249 $debug and warn "item: " . Dumper($item->unblessed);
2250 ModItemTransfer($item->itemnumber, $branch, $returnbranch, $transfer_trigger, { skip_record_index => 1 });
2251 $messages->{'WasTransfered'} = 1;
2253 $messages->{'NeedsTransfer'} = $returnbranch;
2254 $messages->{'TransferTrigger'} = $transfer_trigger;
2258 if ( C4::Context->preference('ClaimReturnedLostValue') ) {
2259 my $claims = Koha::Checkouts::ReturnClaims->search(
2261 itemnumber => $item->id,
2262 resolution => undef,
2266 if ( $claims->count ) {
2267 $messages->{ReturnClaims} = $claims;
2271 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
2272 $indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
2274 if ( $doreturn and $issue ) {
2275 my $checkin = Koha::Old::Checkouts->find($issue->id);
2277 Koha::Plugins->call('after_circ_action', {
2278 action => 'checkin',
2285 return ( $doreturn, $messages, $issue, ( $patron ? $patron->unblessed : {} ));
2288 =head2 MarkIssueReturned
2290 MarkIssueReturned($borrowernumber, $itemnumber, $returndate, $privacy, [$params] );
2292 Unconditionally marks an issue as being returned by
2293 moving the C<issues> row to C<old_issues> and
2294 setting C<returndate> to the current date.
2296 if C<$returndate> is specified (in iso format), it is used as the date
2299 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
2300 the old_issue is immediately anonymised
2302 Ideally, this function would be internal to C<C4::Circulation>,
2303 not exported, but it is currently used in misc/cronjobs/longoverdue.pl
2304 and offline_circ/process_koc.pl.
2306 The last optional parameter allos passing skip_record_index to the item store call.
2310 sub MarkIssueReturned {
2311 my ( $borrowernumber, $itemnumber, $returndate, $privacy, $params ) = @_;
2313 # Retrieve the issue
2314 my $issue = Koha::Checkouts->find( { itemnumber => $itemnumber } ) or return;
2316 return unless $issue->borrowernumber == $borrowernumber; # If the item is checked out to another patron we do not return it
2318 my $issue_id = $issue->issue_id;
2320 my $anonymouspatron;
2321 if ( $privacy && $privacy == 2 ) {
2322 # The default of 0 will not work due to foreign key constraints
2323 # The anonymisation will fail if AnonymousPatron is not a valid entry
2324 # We need to check if the anonymous patron exist, Koha will fail loudly if it does not
2325 # Note that a warning should appear on the about page (System information tab).
2326 $anonymouspatron = C4::Context->preference('AnonymousPatron');
2327 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."
2328 unless Koha::Patrons->find( $anonymouspatron );
2331 my $schema = Koha::Database->schema;
2333 # FIXME Improve the return value and handle it from callers
2334 $schema->txn_do(sub {
2336 my $patron = Koha::Patrons->find( $borrowernumber );
2338 # Update the returndate value
2339 if ( $returndate ) {
2340 $issue->returndate( $returndate )->store->discard_changes; # update and refetch
2343 $issue->returndate( \'NOW()' )->store->discard_changes; # update and refetch
2346 # Create the old_issues entry
2347 my $old_checkout = Koha::Old::Checkout->new($issue->unblessed)->store;
2349 # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
2350 if ( $privacy && $privacy == 2) {
2351 $old_checkout->borrowernumber($anonymouspatron)->store;
2354 # And finally delete the issue
2357 $issue->item->onloan(undef)->store({ log_action => 0, skip_record_index => $params->{skip_record_index} });
2359 if ( C4::Context->preference('StoreLastBorrower') ) {
2360 my $item = Koha::Items->find( $itemnumber );
2361 $item->last_returned_by( $patron );
2364 # Remove any OVERDUES related debarment if the borrower has no overdues
2365 if ( C4::Context->preference('AutoRemoveOverduesRestrictions')
2366 && $patron->debarred
2367 && !$patron->has_overdues
2368 && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2370 DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2378 =head2 _debar_user_on_return
2380 _debar_user_on_return($borrower, $item, $datedue, $returndate);
2382 C<$borrower> borrower hashref
2384 C<$item> item hashref
2386 C<$datedue> date due DateTime object
2388 C<$returndate> DateTime object representing the return time
2390 Internal function, called only by AddReturn that calculates and updates
2391 the user fine days, and debars them if necessary.
2393 Should only be called for overdue returns
2395 Calculation of the debarment date has been moved to a separate subroutine _calculate_new_debar_dt
2400 sub _calculate_new_debar_dt {
2401 my ( $borrower, $item, $dt_due, $return_date ) = @_;
2403 my $branchcode = _GetCircControlBranch( $item, $borrower );
2404 my $circcontrol = C4::Context->preference('CircControl');
2405 my $issuing_rule = Koha::CirculationRules->get_effective_rules(
2406 { categorycode => $borrower->{categorycode},
2407 itemtype => $item->{itype},
2408 branchcode => $branchcode,
2413 'maxsuspensiondays',
2414 'suspension_chargeperiod',
2418 my $finedays = $issuing_rule ? $issuing_rule->{finedays} : undef;
2419 my $unit = $issuing_rule ? $issuing_rule->{lengthunit} : undef;
2420 my $chargeable_units = C4::Overdues::get_chargeable_units($unit, $dt_due, $return_date, $branchcode);
2422 return unless $finedays;
2424 # finedays is in days, so hourly loans must multiply by 24
2425 # thus 1 hour late equals 1 day suspension * finedays rate
2426 $finedays = $finedays * 24 if ( $unit eq 'hours' );
2428 # grace period is measured in the same units as the loan
2430 DateTime::Duration->new( $unit => $issuing_rule->{firstremind} // 0);
2432 my $deltadays = DateTime::Duration->new(
2433 days => $chargeable_units
2436 if ( $deltadays->subtract($grace)->is_positive() ) {
2437 my $suspension_days = $deltadays * $finedays;
2439 if ( defined $issuing_rule->{suspension_chargeperiod} && $issuing_rule->{suspension_chargeperiod} > 1 ) {
2440 # No need to / 1 and do not consider / 0
2441 $suspension_days = DateTime::Duration->new(
2442 days => floor( $suspension_days->in_units('days') / $issuing_rule->{suspension_chargeperiod} )
2446 # If the max suspension days is < than the suspension days
2447 # the suspension days is limited to this maximum period.
2448 my $max_sd = $issuing_rule->{maxsuspensiondays};
2449 if ( defined $max_sd && $max_sd ne '' ) {
2450 $max_sd = DateTime::Duration->new( days => $max_sd );
2451 $suspension_days = $max_sd
2452 if DateTime::Duration->compare( $max_sd, $suspension_days ) < 0;
2455 my ( $has_been_extended );
2456 if ( C4::Context->preference('CumulativeRestrictionPeriods') and $borrower->{debarred} ) {
2457 my $debarment = @{ GetDebarments( { borrowernumber => $borrower->{borrowernumber}, type => 'SUSPENSION' } ) }[0];
2459 $return_date = dt_from_string( $debarment->{expiration}, 'sql' );
2460 $has_been_extended = 1;
2465 # Use the calendar or not to calculate the debarment date
2466 if ( C4::Context->preference('SuspensionsCalendar') eq 'noSuspensionsWhenClosed' ) {
2467 my $calendar = Koha::Calendar->new(
2468 branchcode => $branchcode,
2469 days_mode => 'Calendar'
2471 $new_debar_dt = $calendar->addDuration( $return_date, $suspension_days );
2474 $new_debar_dt = $return_date->clone()->add_duration($suspension_days);
2476 return $new_debar_dt;
2481 sub _debar_user_on_return {
2482 my ( $borrower, $item, $dt_due, $return_date ) = @_;
2484 $return_date //= dt_from_string();
2486 my $new_debar_dt = _calculate_new_debar_dt ($borrower, $item, $dt_due, $return_date);
2488 return unless $new_debar_dt;
2490 Koha::Patron::Debarments::AddUniqueDebarment({
2491 borrowernumber => $borrower->{borrowernumber},
2492 expiration => $new_debar_dt->ymd(),
2493 type => 'SUSPENSION',
2495 # if borrower was already debarred but does not get an extra debarment
2496 my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
2497 my ($new_debarment_str, $is_a_reminder);
2498 if ( $borrower->{debarred} && $borrower->{debarred} eq $patron->is_debarred ) {
2500 $new_debarment_str = $borrower->{debarred};
2502 $new_debarment_str = $new_debar_dt->ymd();
2504 # FIXME Should return a DateTime object
2505 return $new_debarment_str, $is_a_reminder;
2508 =head2 _FixOverduesOnReturn
2510 &_FixOverduesOnReturn($borrowernumber, $itemnumber, $exemptfine, $status);
2512 C<$borrowernumber> borrowernumber
2514 C<$itemnumber> itemnumber
2516 C<$exemptfine> BOOL -- remove overdue charge associated with this issue.
2518 C<$status> ENUM -- reason for fix [ RETURNED, RENEWED, LOST, FORGIVEN ]
2524 sub _FixOverduesOnReturn {
2525 my ( $borrowernumber, $item, $exemptfine, $status ) = @_;
2526 unless( $borrowernumber ) {
2527 warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
2531 warn "_FixOverduesOnReturn() not supplied valid itemnumber";
2535 warn "_FixOverduesOnReturn() not supplied valid status";
2539 my $schema = Koha::Database->schema;
2541 my $result = $schema->txn_do(
2543 # check for overdue fine
2544 my $accountlines = Koha::Account::Lines->search(
2546 borrowernumber => $borrowernumber,
2547 itemnumber => $item,
2548 debit_type_code => 'OVERDUE',
2549 status => 'UNRETURNED'
2552 return 0 unless $accountlines->count; # no warning, there's just nothing to fix
2554 my $accountline = $accountlines->next;
2555 my $payments = $accountline->credits;
2557 my $amountoutstanding = $accountline->amountoutstanding;
2558 if ( $accountline->amount == 0 && $payments->count == 0 ) {
2559 $accountline->delete;
2560 return 0; # no warning, we've just removed a zero value fine (backdated return)
2561 } elsif ($exemptfine && ($amountoutstanding != 0)) {
2562 my $account = Koha::Account->new({patron_id => $borrowernumber});
2563 my $credit = $account->add_credit(
2565 amount => $amountoutstanding,
2566 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
2567 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
2568 interface => C4::Context->interface,
2574 $credit->apply({ debits => [ $accountline ], offset_type => 'Forgiven' });
2576 if (C4::Context->preference("FinesLog")) {
2577 &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
2581 $accountline->status($status);
2582 return $accountline->store();
2589 =head2 _GetCircControlBranch
2591 my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
2595 Return the library code to be used to determine which circulation
2596 policy applies to a transaction. Looks up the CircControl and
2597 HomeOrHoldingBranch system preferences.
2599 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
2601 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
2605 sub _GetCircControlBranch {
2606 my ($item, $borrower) = @_;
2607 my $circcontrol = C4::Context->preference('CircControl');
2610 if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
2611 $branch= C4::Context->userenv->{'branch'};
2612 } elsif ($circcontrol eq 'PatronLibrary') {
2613 $branch=$borrower->{branchcode};
2615 my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
2616 $branch = $item->{$branchfield};
2617 # default to item home branch if holdingbranch is used
2618 # and is not defined
2619 if (!defined($branch) && $branchfield eq 'holdingbranch') {
2620 $branch = $item->{homebranch};
2628 $issue = GetOpenIssue( $itemnumber );
2630 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2632 C<$itemnumber> is the item's itemnumber
2639 my ( $itemnumber ) = @_;
2640 return unless $itemnumber;
2641 my $dbh = C4::Context->dbh;
2642 my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2643 $sth->execute( $itemnumber );
2644 return $sth->fetchrow_hashref();
2648 =head2 GetUpcomingDueIssues
2650 my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2654 sub GetUpcomingDueIssues {
2657 $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2658 my $dbh = C4::Context->dbh;
2661 SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2663 LEFT JOIN items USING (itemnumber)
2664 LEFT JOIN branches ON branches.branchcode =
2666 $statement .= $params->{'owning_library'} ? " items.homebranch " : " issues.branchcode ";
2667 $statement .= " WHERE returndate is NULL AND TO_DAYS( date_due )-TO_DAYS( NOW() ) BETWEEN 0 AND ?";
2668 my @bind_parameters = ( $params->{'days_in_advance'} );
2670 my $sth = $dbh->prepare( $statement );
2671 $sth->execute( @bind_parameters );
2672 my $upcoming_dues = $sth->fetchall_arrayref({});
2674 return $upcoming_dues;
2677 =head2 CanBookBeRenewed
2679 ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber[, $override_limit]);
2681 Find out whether a borrowed item may be renewed.
2683 C<$borrowernumber> is the borrower number of the patron who currently
2684 has the item on loan.
2686 C<$itemnumber> is the number of the item to renew.
2688 C<$override_limit>, if supplied with a true value, causes
2689 the limit on the number of times that the loan can be renewed
2690 (as controlled by the item type) to be ignored. Overriding also allows
2691 to renew sooner than "No renewal before" and to manually renew loans
2692 that are automatically renewed.
2694 C<$CanBookBeRenewed> returns a true value if the item may be renewed. The
2695 item must currently be on loan to the specified borrower; renewals
2696 must be allowed for the item's type; and the borrower must not have
2697 already renewed the loan. $error will contain the reason the renewal can not proceed
2701 sub CanBookBeRenewed {
2702 my ( $borrowernumber, $itemnumber, $override_limit, $cron ) = @_;
2704 my $dbh = C4::Context->dbh;
2706 my $auto_renew = "no";
2708 my $item = Koha::Items->find($itemnumber) or return ( 0, 'no_item' );
2709 my $issue = $item->checkout or return ( 0, 'no_checkout' );
2710 return ( 0, 'onsite_checkout' ) if $issue->onsite_checkout;
2711 return ( 0, 'item_denied_renewal') if _item_denied_renewal({ item => $item });
2713 my $patron = $issue->patron or return;
2715 # override_limit will override anything else except on_reserve
2716 unless ( $override_limit ){
2717 my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
2718 my $issuing_rule = Koha::CirculationRules->get_effective_rules(
2720 categorycode => $patron->categorycode,
2721 itemtype => $item->effective_itemtype,
2722 branchcode => $branchcode,
2725 'no_auto_renewal_after',
2726 'no_auto_renewal_after_hard_limit',
2729 'unseen_renewals_allowed'
2734 return ( 0, "too_many" )
2735 if not $issuing_rule->{renewalsallowed} or $issuing_rule->{renewalsallowed} <= $issue->renewals;
2737 return ( 0, "too_unseen" )
2738 if C4::Context->preference('UnseenRenewals') &&
2739 $issuing_rule->{unseen_renewals_allowed} &&
2740 $issuing_rule->{unseen_renewals_allowed} <= $issue->unseen_renewals;
2742 my $overduesblockrenewing = C4::Context->preference('OverduesBlockRenewing');
2743 my $restrictionblockrenewing = C4::Context->preference('RestrictionBlockRenewing');
2744 $patron = Koha::Patrons->find($borrowernumber); # FIXME Is this really useful?
2745 my $restricted = $patron->is_debarred;
2746 my $hasoverdues = $patron->has_overdues;
2748 if ( $restricted and $restrictionblockrenewing ) {
2749 return ( 0, 'restriction');
2750 } elsif ( ($hasoverdues and $overduesblockrenewing eq 'block') || ($issue->is_overdue and $overduesblockrenewing eq 'blockitem') ) {
2751 return ( 0, 'overdue');
2754 if ( $issue->auto_renew && $patron->autorenew_checkouts ) {
2756 if ( $patron->category->effective_BlockExpiredPatronOpacActions and $patron->is_expired ) {
2757 return ( 0, 'auto_account_expired' );
2760 if ( defined $issuing_rule->{no_auto_renewal_after}
2761 and $issuing_rule->{no_auto_renewal_after} ne "" ) {
2762 # Get issue_date and add no_auto_renewal_after
2763 # If this is greater than today, it's too late for renewal.
2764 my $maximum_renewal_date = dt_from_string($issue->issuedate, 'sql');
2765 $maximum_renewal_date->add(
2766 $issuing_rule->{lengthunit} => $issuing_rule->{no_auto_renewal_after}
2768 my $now = dt_from_string;
2769 if ( $now >= $maximum_renewal_date ) {
2770 return ( 0, "auto_too_late" );
2773 if ( defined $issuing_rule->{no_auto_renewal_after_hard_limit}
2774 and $issuing_rule->{no_auto_renewal_after_hard_limit} ne "" ) {
2775 # If no_auto_renewal_after_hard_limit is >= today, it's also too late for renewal
2776 if ( dt_from_string >= dt_from_string( $issuing_rule->{no_auto_renewal_after_hard_limit} ) ) {
2777 return ( 0, "auto_too_late" );
2781 if ( C4::Context->preference('OPACFineNoRenewalsBlockAutoRenew') ) {
2782 my $fine_no_renewals = C4::Context->preference("OPACFineNoRenewals");
2783 my $amountoutstanding =
2784 C4::Context->preference("OPACFineNoRenewalsIncludeCredit")
2785 ? $patron->account->balance
2786 : $patron->account->outstanding_debits->total_outstanding;
2787 if ( $amountoutstanding and $amountoutstanding > $fine_no_renewals ) {
2788 return ( 0, "auto_too_much_oweing" );
2793 if ( defined $issuing_rule->{norenewalbefore}
2794 and $issuing_rule->{norenewalbefore} ne "" )
2797 # Calculate soonest renewal by subtracting 'No renewal before' from due date
2798 my $soonestrenewal = dt_from_string( $issue->date_due, 'sql' )->subtract(
2799 $issuing_rule->{lengthunit} => $issuing_rule->{norenewalbefore} );
2801 # Depending on syspref reset the exact time, only check the date
2802 if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
2803 and $issuing_rule->{lengthunit} eq 'days' )
2805 $soonestrenewal->truncate( to => 'day' );
2808 if ( $soonestrenewal > dt_from_string() )
2810 $auto_renew = ($issue->auto_renew && $patron->autorenew_checkouts) ? "auto_too_soon" : "too_soon";
2812 elsif ( $issue->auto_renew && $patron->autorenew_checkouts ) {
2817 # Fallback for automatic renewals:
2818 # If norenewalbefore is undef, don't renew before due date.
2819 if ( $issue->auto_renew && $auto_renew eq "no" && $patron->autorenew_checkouts ) {
2820 my $now = dt_from_string;
2821 if ( $now >= dt_from_string( $issue->date_due, 'sql' ) ){
2824 $auto_renew = "auto_too_soon";
2829 my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2831 # If next hold is non priority, then check if any hold with priority (non_priority = 0) exists for the same biblionumber.
2832 if ( $resfound && $resrec->{non_priority} ) {
2833 $resfound = Koha::Holds->search(
2834 { biblionumber => $resrec->{biblionumber}, non_priority => 0 } )
2840 # This item can fill one or more unfilled reserve, can those unfilled reserves
2841 # all be filled by other available items?
2843 && C4::Context->preference('AllowRenewalIfOtherItemsAvailable') )
2845 my $schema = Koha::Database->new()->schema();
2847 my $item_holds = $schema->resultset('Reserve')->search( { itemnumber => $itemnumber, found => undef } )->count();
2849 # There is an item level hold on this item, no other item can fill the hold
2854 # Get all other items that could possibly fill reserves
2855 my @itemnumbers = $schema->resultset('Item')->search(
2857 biblionumber => $resrec->{biblionumber},
2860 -not => { itemnumber => $itemnumber }
2862 { columns => 'itemnumber' }
2863 )->get_column('itemnumber')->all();
2865 # Get all other reserves that could have been filled by this item
2866 my @borrowernumbers;
2868 my ( $reserve_found, $reserve, undef ) =
2869 C4::Reserves::CheckReserves( $itemnumber, undef, undef, \@borrowernumbers );
2871 if ($reserve_found) {
2872 push( @borrowernumbers, $reserve->{borrowernumber} );
2879 # If the count of the union of the lists of reservable items for each borrower
2880 # is equal or greater than the number of borrowers, we know that all reserves
2881 # can be filled with available items. We can get the union of the sets simply
2882 # by pushing all the elements onto an array and removing the duplicates.
2885 ITEM: foreach my $itemnumber (@itemnumbers) {
2886 my $item = Koha::Items->find( $itemnumber );
2887 next if IsItemOnHoldAndFound( $itemnumber );
2888 for my $borrowernumber (@borrowernumbers) {
2889 my $patron = $patrons{$borrowernumber} //= Koha::Patrons->find( $borrowernumber );
2890 next unless IsAvailableForItemLevelRequest($item, $patron);
2891 next unless CanItemBeReserved($borrowernumber,$itemnumber);
2893 push @reservable, $itemnumber;
2894 if (@reservable >= @borrowernumbers) {
2903 if( $cron ) { #The cron wants to return 'too_soon' over 'on_reserve'
2904 return ( 0, $auto_renew ) if $auto_renew =~ 'too_soon';#$auto_renew ne "no" && $auto_renew ne "ok";
2905 return ( 0, "on_reserve" ) if $resfound; # '' when no hold was found
2906 } else { # For other purposes we want 'on_reserve' before 'too_soon'
2907 return ( 0, "on_reserve" ) if $resfound; # '' when no hold was found
2908 return ( 0, $auto_renew ) if $auto_renew =~ 'too_soon';#$auto_renew ne "no" && $auto_renew ne "ok";
2911 return ( 0, "auto_renew" ) if $auto_renew eq "ok" && !$override_limit; # 0 if auto-renewal should not succeed
2913 return ( 1, undef );
2918 &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate], [$seen]);
2922 C<$borrowernumber> is the borrower number of the patron who currently
2925 C<$itemnumber> is the number of the item to renew.
2927 C<$branch> is the library where the renewal took place (if any).
2928 The library that controls the circ policies for the renewal is retrieved from the issues record.
2930 C<$datedue> can be a DateTime object used to set the due date.
2932 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate. If
2933 this parameter is not supplied, lastreneweddate is set to the current date.
2935 C<$skipfinecalc> is an optional boolean. There may be circumstances where, even if the
2936 CalculateFinesOnReturn syspref is enabled, we don't want to calculate fines upon renew,
2937 for example, when we're renewing as a result of a fine being paid (see RenewAccruingItemWhenPaid
2940 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2941 from the book's item type.
2943 C<$seen> is a boolean flag indicating if the item was seen or not during the renewal. This
2944 informs the incrementing of the unseen_renewals column. If this flag is not supplied, we
2945 fallback to a true value
2950 my $borrowernumber = shift;
2951 my $itemnumber = shift or return;
2953 my $datedue = shift;
2954 my $lastreneweddate = shift || dt_from_string();
2955 my $skipfinecalc = shift;
2958 # Fallback on a 'seen' renewal
2959 $seen = defined $seen && $seen == 0 ? 0 : 1;
2961 my $item_object = Koha::Items->find($itemnumber) or return;
2962 my $biblio = $item_object->biblio;
2963 my $issue = $item_object->checkout;
2964 my $item_unblessed = $item_object->unblessed;
2966 my $dbh = C4::Context->dbh;
2968 return unless $issue;
2970 $borrowernumber ||= $issue->borrowernumber;
2972 if ( defined $datedue && ref $datedue ne 'DateTime' ) {
2973 carp 'Invalid date passed to AddRenewal.';
2977 my $patron = Koha::Patrons->find( $borrowernumber ) or return; # FIXME Should do more than just return
2978 my $patron_unblessed = $patron->unblessed;
2980 my $circ_library = Koha::Libraries->find( _GetCircControlBranch($item_unblessed, $patron_unblessed) );
2982 my $schema = Koha::Database->schema;
2983 $schema->txn_do(sub{
2985 if ( !$skipfinecalc && C4::Context->preference('CalculateFinesOnReturn') ) {
2986 _CalculateAndUpdateFine( { issue => $issue, item => $item_unblessed, borrower => $patron_unblessed } );
2988 _FixOverduesOnReturn( $borrowernumber, $itemnumber, undef, 'RENEWED' );
2990 # If the due date wasn't specified, calculate it by adding the
2991 # book's loan length to today's date or the current due date
2992 # based on the value of the RenewalPeriodBase syspref.
2993 my $itemtype = $item_object->effective_itemtype;
2996 $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
2997 dt_from_string( $issue->date_due, 'sql' ) :
2999 $datedue = CalcDateDue($datedue, $itemtype, $circ_library->branchcode, $patron_unblessed, 'is a renewal');
3002 my $fees = Koha::Charges::Fees->new(
3005 library => $circ_library,
3006 item => $item_object,
3007 from_date => dt_from_string( $issue->date_due, 'sql' ),
3008 to_date => dt_from_string($datedue),
3012 # Increment the unseen renewals, if appropriate
3013 # We only do so if the syspref is enabled and
3014 # a maximum value has been set in the circ rules
3015 my $unseen_renewals = $issue->unseen_renewals;
3016 if (C4::Context->preference('UnseenRenewals')) {
3017 my $rule = Koha::CirculationRules->get_effective_rule(
3018 { categorycode => $patron->categorycode,
3019 itemtype => $item_object->effective_itemtype,
3020 branchcode => $circ_library->branchcode,
3021 rule_name => 'unseen_renewals_allowed'
3024 if (!$seen && $rule && $rule->rule_value) {
3027 # If the renewal is seen, unseen should revert to 0
3028 $unseen_renewals = 0;
3032 # Update the issues record to have the new due date, and a new count
3033 # of how many times it has been renewed.
3034 my $renews = ( $issue->renewals || 0 ) + 1;
3035 my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, unseen_renewals = ?, lastreneweddate = ?
3036 WHERE borrowernumber=?
3040 $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $unseen_renewals, $lastreneweddate, $borrowernumber, $itemnumber );
3042 # Update the renewal count on the item, and tell zebra to reindex
3043 $renews = ( $item_object->renewals || 0 ) + 1;
3044 $item_object->renewals($renews);
3045 $item_object->onloan($datedue);
3046 $item_object->store({ log_action => 0 });
3048 # Charge a new rental fee, if applicable
3049 my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
3050 if ( $charge > 0 ) {
3051 AddIssuingCharge($issue, $charge, 'RENT_RENEW');
3054 # Charge a new accumulate rental fee, if applicable
3055 my $itemtype_object = Koha::ItemTypes->find( $itemtype );
3056 if ( $itemtype_object ) {
3057 my $accumulate_charge = $fees->accumulate_rentalcharge();
3058 if ( $accumulate_charge > 0 ) {
3059 AddIssuingCharge( $issue, $accumulate_charge, 'RENT_DAILY_RENEW' )
3061 $charge += $accumulate_charge;
3064 # Send a renewal slip according to checkout alert preferencei
3065 if ( C4::Context->preference('RenewalSendNotice') eq '1' ) {
3066 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
3068 branchcode => $branch,
3069 categorycode => $patron->categorycode,
3070 item_type => $itemtype,
3071 notification => 'CHECKOUT',
3073 if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
3074 SendCirculationAlert(
3077 item => $item_unblessed,
3078 borrower => $patron->unblessed,
3085 # Remove any OVERDUES related debarment if the borrower has no overdues
3087 && $patron->is_debarred
3088 && ! $patron->has_overdues
3089 && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
3091 DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
3094 # Add the renewal to stats
3097 branch => $item_object->renewal_branchcode({branch => $branch}),
3100 itemnumber => $itemnumber,
3101 itemtype => $itemtype,
3102 location => $item_object->location,
3103 borrowernumber => $borrowernumber,
3104 ccode => $item_object->ccode,
3109 logaction("CIRCULATION", "RENEWAL", $borrowernumber, $itemnumber) if C4::Context->preference("RenewalLog");
3111 Koha::Plugins->call('after_circ_action', {
3112 action => 'renewal',
3114 checkout => $issue->get_from_storage
3123 # check renewal status
3124 my ( $bornum, $itemno ) = @_;
3125 my $dbh = C4::Context->dbh;
3127 my $unseencount = 0;
3128 my $renewsallowed = 0;
3129 my $unseenallowed = 0;
3133 my $patron = Koha::Patrons->find( $bornum );
3134 my $item = Koha::Items->find($itemno);
3136 return (0, 0, 0, 0, 0, 0) unless $patron or $item; # Wrong call, no renewal allowed
3138 # Look in the issues table for this item, lent to this borrower,
3139 # and not yet returned.
3141 # FIXME - I think this function could be redone to use only one SQL call.
3142 my $sth = $dbh->prepare(
3143 "select * from issues
3144 where (borrowernumber = ?)
3145 and (itemnumber = ?)"
3147 $sth->execute( $bornum, $itemno );
3148 my $data = $sth->fetchrow_hashref;
3149 $renewcount = $data->{'renewals'} if $data->{'renewals'};
3150 $unseencount = $data->{'unseen_renewals'} if $data->{'unseen_renewals'};
3151 # $item and $borrower should be calculated
3152 my $branchcode = _GetCircControlBranch($item->unblessed, $patron->unblessed);
3154 my $rules = Koha::CirculationRules->get_effective_rules(
3156 categorycode => $patron->categorycode,
3157 itemtype => $item->effective_itemtype,
3158 branchcode => $branchcode,
3159 rules => [ 'renewalsallowed', 'unseen_renewals_allowed' ]
3162 $renewsallowed = $rules ? $rules->{renewalsallowed} : 0;
3163 $unseenallowed = $rules->{unseen_renewals_allowed} ?
3164 $rules->{unseen_renewals_allowed} :
3166 $renewsleft = $renewsallowed - $renewcount;
3167 $unseenleft = $unseenallowed - $unseencount;
3168 if($renewsleft < 0){ $renewsleft = 0; }
3169 if($unseenleft < 0){ $unseenleft = 0; }
3180 =head2 GetSoonestRenewDate
3182 $NoRenewalBeforeThisDate = &GetSoonestRenewDate($borrowernumber, $itemnumber);
3184 Find out the soonest possible renew date of a borrowed item.
3186 C<$borrowernumber> is the borrower number of the patron who currently
3187 has the item on loan.
3189 C<$itemnumber> is the number of the item to renew.
3191 C<$GetSoonestRenewDate> returns the DateTime of the soonest possible
3192 renew date, based on the value "No renewal before" of the applicable
3193 issuing rule. Returns the current date if the item can already be
3194 renewed, and returns undefined if the borrower, loan, or item
3199 sub GetSoonestRenewDate {
3200 my ( $borrowernumber, $itemnumber ) = @_;
3202 my $dbh = C4::Context->dbh;
3204 my $item = Koha::Items->find($itemnumber) or return;
3205 my $itemissue = $item->checkout or return;
3207 $borrowernumber ||= $itemissue->borrowernumber;
3208 my $patron = Koha::Patrons->find( $borrowernumber )
3211 my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
3212 my $issuing_rule = Koha::CirculationRules->get_effective_rules(
3213 { categorycode => $patron->categorycode,
3214 itemtype => $item->effective_itemtype,
3215 branchcode => $branchcode,
3223 my $now = dt_from_string;
3224 return $now unless $issuing_rule;
3226 if ( defined $issuing_rule->{norenewalbefore}
3227 and $issuing_rule->{norenewalbefore} ne "" )
3229 my $soonestrenewal =
3230 dt_from_string( $itemissue->date_due )->subtract(
3231 $issuing_rule->{lengthunit} => $issuing_rule->{norenewalbefore} );
3233 if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
3234 and $issuing_rule->{lengthunit} eq 'days' )
3236 $soonestrenewal->truncate( to => 'day' );
3238 return $soonestrenewal if $now < $soonestrenewal;
3243 =head2 GetLatestAutoRenewDate
3245 $NoAutoRenewalAfterThisDate = &GetLatestAutoRenewDate($borrowernumber, $itemnumber);
3247 Find out the latest possible auto renew date of a borrowed item.
3249 C<$borrowernumber> is the borrower number of the patron who currently
3250 has the item on loan.
3252 C<$itemnumber> is the number of the item to renew.
3254 C<$GetLatestAutoRenewDate> returns the DateTime of the latest possible
3255 auto renew date, based on the value "No auto renewal after" and the "No auto
3256 renewal after (hard limit) of the applicable issuing rule.
3257 Returns undef if there is no date specify in the circ rules or if the patron, loan,
3258 or item cannot be found.
3262 sub GetLatestAutoRenewDate {
3263 my ( $borrowernumber, $itemnumber ) = @_;
3265 my $dbh = C4::Context->dbh;
3267 my $item = Koha::Items->find($itemnumber) or return;
3268 my $itemissue = $item->checkout or return;
3270 $borrowernumber ||= $itemissue->borrowernumber;
3271 my $patron = Koha::Patrons->find( $borrowernumber )
3274 my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
3275 my $circulation_rules = Koha::CirculationRules->get_effective_rules(
3277 categorycode => $patron->categorycode,
3278 itemtype => $item->effective_itemtype,
3279 branchcode => $branchcode,
3281 'no_auto_renewal_after',
3282 'no_auto_renewal_after_hard_limit',
3288 return unless $circulation_rules;
3290 if ( not $circulation_rules->{no_auto_renewal_after}
3291 or $circulation_rules->{no_auto_renewal_after} eq '' )
3292 and ( not $circulation_rules->{no_auto_renewal_after_hard_limit}
3293 or $circulation_rules->{no_auto_renewal_after_hard_limit} eq '' );
3295 my $maximum_renewal_date;
3296 if ( $circulation_rules->{no_auto_renewal_after} ) {
3297 $maximum_renewal_date = dt_from_string($itemissue->issuedate);
3298 $maximum_renewal_date->add(
3299 $circulation_rules->{lengthunit} => $circulation_rules->{no_auto_renewal_after}
3303 if ( $circulation_rules->{no_auto_renewal_after_hard_limit} ) {
3304 my $dt = dt_from_string( $circulation_rules->{no_auto_renewal_after_hard_limit} );
3305 $maximum_renewal_date = $dt if not $maximum_renewal_date or $maximum_renewal_date > $dt;
3307 return $maximum_renewal_date;
3311 =head2 GetIssuingCharges
3313 ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
3315 Calculate how much it would cost for a given patron to borrow a given
3316 item, including any applicable discounts.
3318 C<$itemnumber> is the item number of item the patron wishes to borrow.
3320 C<$borrowernumber> is the patron's borrower number.
3322 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
3323 and C<$item_type> is the code for the item's item type (e.g., C<VID>
3328 sub GetIssuingCharges {
3330 # calculate charges due
3331 my ( $itemnumber, $borrowernumber ) = @_;
3333 my $dbh = C4::Context->dbh;
3336 # Get the book's item type and rental charge (via its biblioitem).
3337 my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
3338 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
3339 $charge_query .= (C4::Context->preference('item-level_itypes'))
3340 ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
3341 : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
3343 $charge_query .= ' WHERE items.itemnumber =?';
3345 my $sth = $dbh->prepare($charge_query);
3346 $sth->execute($itemnumber);
3347 if ( my $item_data = $sth->fetchrow_hashref ) {
3348 $item_type = $item_data->{itemtype};
3349 $charge = $item_data->{rentalcharge};
3350 # FIXME This should follow CircControl
3351 my $branch = C4::Context::mybranch();
3352 my $patron = Koha::Patrons->find( $borrowernumber );
3353 my $discount = Koha::CirculationRules->get_effective_rule({
3354 categorycode => $patron->categorycode,
3355 branchcode => $branch,
3356 itemtype => $item_type,
3357 rule_name => 'rentaldiscount'
3360 $charge = ( $charge * ( 100 - $discount->rule_value ) ) / 100;
3363 $charge = sprintf '%.2f', $charge; # ensure no fractions of a penny returned
3367 return ( $charge, $item_type );
3370 =head2 AddIssuingCharge
3372 &AddIssuingCharge( $checkout, $charge, $type )
3376 sub AddIssuingCharge {
3377 my ( $checkout, $charge, $type ) = @_;
3379 # FIXME What if checkout does not exist?
3381 my $account = Koha::Account->new({ patron_id => $checkout->borrowernumber });
3382 my $accountline = $account->add_debit(
3386 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
3387 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
3388 interface => C4::Context->interface,
3390 item_id => $checkout->itemnumber,
3391 issue_id => $checkout->issue_id,
3398 GetTransfers($itemnumber);
3403 my ($itemnumber) = @_;
3405 my $dbh = C4::Context->dbh;
3412 FROM branchtransfers
3413 WHERE itemnumber = ?
3414 AND datearrived IS NULL
3416 my $sth = $dbh->prepare($query);
3417 $sth->execute($itemnumber);
3418 my @row = $sth->fetchrow_array();
3422 =head2 GetTransfersFromTo
3424 @results = GetTransfersFromTo($frombranch,$tobranch);
3426 Returns the list of pending transfers between $from and $to branch
3430 sub GetTransfersFromTo {
3431 my ( $frombranch, $tobranch ) = @_;
3432 return unless ( $frombranch && $tobranch );
3433 my $dbh = C4::Context->dbh;
3435 SELECT branchtransfer_id,itemnumber,datesent,frombranch
3436 FROM branchtransfers
3439 AND datearrived IS NULL
3441 my $sth = $dbh->prepare($query);
3442 $sth->execute( $frombranch, $tobranch );
3445 while ( my $data = $sth->fetchrow_hashref ) {
3446 push @gettransfers, $data;
3448 return (@gettransfers);
3451 =head2 DeleteTransfer
3453 &DeleteTransfer($itemnumber);
3457 sub DeleteTransfer {
3458 my ($itemnumber) = @_;
3459 return unless $itemnumber;
3460 my $dbh = C4::Context->dbh;
3461 my $sth = $dbh->prepare(
3462 "DELETE FROM branchtransfers
3464 AND datearrived IS NULL "
3466 return $sth->execute($itemnumber);
3469 =head2 SendCirculationAlert
3471 Send out a C<check-in> or C<checkout> alert using the messaging system.
3479 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
3483 Hashref of information about the item being checked in or out.
3487 Hashref of information about the borrower of the item.
3491 The branchcode from where the checkout or check-in took place.
3497 SendCirculationAlert({
3500 borrower => $borrower,
3506 sub SendCirculationAlert {
3508 my ($type, $item, $borrower, $branch) =
3509 ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
3510 my %message_name = (
3511 CHECKIN => 'Item_Check_in',
3512 CHECKOUT => 'Item_Checkout',
3513 RENEWAL => 'Item_Checkout',
3515 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
3516 borrowernumber => $borrower->{borrowernumber},
3517 message_name => $message_name{$type},
3519 my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues';
3521 my $schema = Koha::Database->new->schema;
3522 my @transports = keys %{ $borrower_preferences->{transports} };
3524 # From the MySQL doc:
3525 # LOCK TABLES is not transaction-safe and implicitly commits any active transaction before attempting to lock the tables.
3526 # If the LOCK/UNLOCK statements are executed from tests, the current transaction will be committed.
3527 # To avoid that we need to guess if this code is execute from tests or not (yes it is a bit hacky)
3528 my $do_not_lock = ( exists $ENV{_} && $ENV{_} =~ m|prove| ) || $ENV{KOHA_TESTING};
3530 for my $mtt (@transports) {
3531 my $letter = C4::Letters::GetPreparedLetter (
3532 module => 'circulation',
3533 letter_code => $type,
3534 branchcode => $branch,
3535 message_transport_type => $mtt,
3536 lang => $borrower->{lang},
3538 $issues_table => $item->{itemnumber},
3539 'items' => $item->{itemnumber},
3540 'biblio' => $item->{biblionumber},
3541 'biblioitems' => $item->{biblionumber},
3542 'borrowers' => $borrower,
3543 'branches' => $branch,
3547 C4::Context->dbh->do(q|LOCK TABLE message_queue READ|) unless $do_not_lock;
3548 C4::Context->dbh->do(q|LOCK TABLE message_queue WRITE|) unless $do_not_lock;
3549 my $message = C4::Message->find_last_message($borrower, $type, $mtt);
3550 unless ( $message ) {
3551 C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3552 C4::Message->enqueue($letter, $borrower, $mtt);
3554 $message->append($letter);
3557 C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3563 =head2 updateWrongTransfer
3565 $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
3567 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
3571 sub updateWrongTransfer {
3572 my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
3573 my $dbh = C4::Context->dbh;
3574 # first step validate the actual line of transfert .
3577 "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
3579 $sth->execute($FromLibrary,$itemNumber);
3581 # second step create a new line of branchtransfer to the right location .
3582 ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
3584 #third step changing holdingbranch of item
3585 my $item = Koha::Items->find($itemNumber)->holdingbranch($FromLibrary)->store;
3590 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
3592 this function calculates the due date given the start date and configured circulation rules,
3593 checking against the holidays calendar as per the daysmode circulation rule.
3594 C<$startdate> = DateTime object representing start date of loan period (assumed to be today)
3595 C<$itemtype> = itemtype code of item in question
3596 C<$branch> = location whose calendar to use
3597 C<$borrower> = Borrower object
3598 C<$isrenewal> = Boolean: is true if we want to calculate the date due for a renewal. Else is false.
3603 my ( $startdate, $itemtype, $branch, $borrower, $isrenewal ) = @_;
3607 # loanlength now a href
3609 GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
3611 my $length_key = ( $isrenewal and defined $loanlength->{renewalperiod} )
3617 if (ref $startdate ne 'DateTime' ) {
3618 $datedue = dt_from_string($datedue);
3620 $datedue = $startdate->clone;
3623 $datedue = dt_from_string()->truncate( to => 'minute' );
3627 my $daysmode = Koha::CirculationRules->get_effective_daysmode(
3629 categorycode => $borrower->{categorycode},
3630 itemtype => $itemtype,
3631 branchcode => $branch,
3635 # calculate the datedue as normal
3636 if ( $daysmode eq 'Days' )
3637 { # ignoring calendar
3638 if ( $loanlength->{lengthunit} eq 'hours' ) {
3639 $datedue->add( hours => $loanlength->{$length_key} );
3641 $datedue->add( days => $loanlength->{$length_key} );
3642 $datedue->set_hour(23);
3643 $datedue->set_minute(59);
3647 if ($loanlength->{lengthunit} eq 'hours') {
3648 $dur = DateTime::Duration->new( hours => $loanlength->{$length_key});
3651 $dur = DateTime::Duration->new( days => $loanlength->{$length_key});
3653 my $calendar = Koha::Calendar->new( branchcode => $branch, days_mode => $daysmode );
3654 $datedue = $calendar->addDuration( $datedue, $dur, $loanlength->{lengthunit} );
3655 if ($loanlength->{lengthunit} eq 'days') {
3656 $datedue->set_hour(23);
3657 $datedue->set_minute(59);
3661 # if Hard Due Dates are used, retrieve them and apply as necessary
3662 my ( $hardduedate, $hardduedatecompare ) =
3663 GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
3664 if ($hardduedate) { # hardduedates are currently dates
3665 $hardduedate->truncate( to => 'minute' );
3666 $hardduedate->set_hour(23);
3667 $hardduedate->set_minute(59);
3668 my $cmp = DateTime->compare( $hardduedate, $datedue );
3670 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
3671 # if the calculated date is before the 'after' Hard Due Date (floor), override
3672 # if the hard due date is set to 'exactly', overrride
3673 if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
3674 $datedue = $hardduedate->clone;
3677 # in all other cases, keep the date due as it is
3681 # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3682 if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3683 my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso', 'floating');
3684 if( $expiry_dt ) { #skip empty expiry date..
3685 $expiry_dt->set( hour => 23, minute => 59);
3686 my $d1= $datedue->clone->set_time_zone('floating');
3687 if ( DateTime->compare( $d1, $expiry_dt ) == 1 ) {
3688 $datedue = $expiry_dt->clone->set_time_zone( C4::Context->tz );
3691 if ( $daysmode ne 'Days' ) {
3692 my $calendar = Koha::Calendar->new( branchcode => $branch, days_mode => $daysmode );
3693 if ( $calendar->is_holiday($datedue) ) {
3694 # Don't return on a closed day
3695 $datedue = $calendar->prev_open_days( $datedue, 1 );
3704 sub CheckValidBarcode{
3706 my $dbh = C4::Context->dbh;
3707 my $query=qq|SELECT count(*)
3711 my $sth = $dbh->prepare($query);
3712 $sth->execute($barcode);
3713 my $exist=$sth->fetchrow ;
3717 =head2 IsBranchTransferAllowed
3719 $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3721 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3723 Deprecated in favor of Koha::Item::Transfer::Limits->find/search and
3724 Koha::Item->can_be_transferred.
3728 sub IsBranchTransferAllowed {
3729 my ( $toBranch, $fromBranch, $code ) = @_;
3731 if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3733 my $limitType = C4::Context->preference("BranchTransferLimitsType");
3734 my $dbh = C4::Context->dbh;
3736 my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3737 $sth->execute( $toBranch, $fromBranch, $code );
3738 my $limit = $sth->fetchrow_hashref();
3740 ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3741 if ( $limit->{'limitId'} ) {
3748 =head2 CreateBranchTransferLimit
3750 CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3752 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3754 Deprecated in favor of Koha::Item::Transfer::Limit->new.
3758 sub CreateBranchTransferLimit {
3759 my ( $toBranch, $fromBranch, $code ) = @_;
3760 return unless defined($toBranch) && defined($fromBranch);
3761 my $limitType = C4::Context->preference("BranchTransferLimitsType");
3763 my $dbh = C4::Context->dbh;
3765 my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3766 return $sth->execute( $code, $toBranch, $fromBranch );
3769 =head2 DeleteBranchTransferLimits
3771 my $result = DeleteBranchTransferLimits($frombranch);
3773 Deletes all the library transfer limits for one library. Returns the
3774 number of limits deleted, 0e0 if no limits were deleted, or undef if
3775 no arguments are supplied.
3777 Deprecated in favor of Koha::Item::Transfer::Limits->search({
3778 fromBranch => $fromBranch
3783 sub DeleteBranchTransferLimits {
3785 return unless defined $branch;
3786 my $dbh = C4::Context->dbh;
3787 my $sth = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3788 return $sth->execute($branch);
3792 my ( $borrowernumber, $itemnum ) = @_;
3793 MarkIssueReturned( $borrowernumber, $itemnum );
3798 LostItem( $itemnumber, $mark_lost_from, $force_mark_returned, [$params] );
3800 The final optional parameter, C<$params>, expected to contain
3801 'skip_record_index' key, which relayed down to Koha::Item/store,
3802 there it prevents calling of ModZebra index_records,
3803 which takes most of the time in batch adds/deletes: index_records better
3804 to be called later in C<additem.pl> after the whole loop.
3807 skip_record_index => 1|0
3812 my ($itemnumber, $mark_lost_from, $force_mark_returned, $params) = @_;
3814 unless ( $mark_lost_from ) {
3815 # Temporary check to avoid regressions
3816 die q|LostItem called without $mark_lost_from, check the API.|;
3820 if ( $force_mark_returned ) {
3823 my $pref = C4::Context->preference('MarkLostItemsAsReturned') // q{};
3824 $mark_returned = ( $pref =~ m|$mark_lost_from| );
3827 my $dbh = C4::Context->dbh();
3828 my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title
3830 JOIN items USING (itemnumber)
3831 JOIN biblio USING (biblionumber)
3832 WHERE issues.itemnumber=?");
3833 $sth->execute($itemnumber);
3834 my $issues=$sth->fetchrow_hashref();
3836 # If a borrower lost the item, add a replacement cost to the their record
3837 if ( my $borrowernumber = $issues->{borrowernumber} ){
3838 my $patron = Koha::Patrons->find( $borrowernumber );
3840 my $fix = _FixOverduesOnReturn($borrowernumber, $itemnumber, C4::Context->preference('WhenLostForgiveFine'), 'LOST');
3841 defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $itemnumber...) failed!"; # zero is OK, check defined
3843 if (C4::Context->preference('WhenLostChargeReplacementFee')){
3844 C4::Accounts::chargelostitem(
3847 $issues->{'replacementprice'},
3848 sprintf( "%s %s %s",
3849 $issues->{'title'} || q{},
3850 $issues->{'barcode'} || q{},
3851 $issues->{'itemcallnumber'} || q{},
3854 #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3855 #warn " $issues->{'borrowernumber'} / $itemnumber ";
3858 MarkIssueReturned($borrowernumber,$itemnumber,undef,$patron->privacy) if $mark_returned;
3861 #When item is marked lost automatically cancel its outstanding transfers and set items holdingbranch to the transfer source branch (frombranch)
3862 if (my ( $datesent,$frombranch,$tobranch ) = GetTransfers($itemnumber)) {
3863 Koha::Items->find($itemnumber)->holdingbranch($frombranch)->store({ skip_record_index => $params->{skip_record_index} });
3865 my $transferdeleted = DeleteTransfer($itemnumber);
3868 sub GetOfflineOperations {
3869 my $dbh = C4::Context->dbh;
3870 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3871 $sth->execute(C4::Context->userenv->{'branch'});
3872 my $results = $sth->fetchall_arrayref({});
3876 sub GetOfflineOperation {
3877 my $operationid = shift;
3878 return unless $operationid;
3879 my $dbh = C4::Context->dbh;
3880 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3881 $sth->execute( $operationid );
3882 return $sth->fetchrow_hashref;
3885 sub AddOfflineOperation {
3886 my ( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount ) = @_;
3887 my $dbh = C4::Context->dbh;
3888 my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber, amount) VALUES(?,?,?,?,?,?,?)");
3889 $sth->execute( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount );
3893 sub DeleteOfflineOperation {
3894 my $dbh = C4::Context->dbh;
3895 my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3896 $sth->execute( shift );
3900 sub ProcessOfflineOperation {
3901 my $operation = shift;
3904 if ( $operation->{action} eq 'return' ) {
3905 $report = ProcessOfflineReturn( $operation );
3906 } elsif ( $operation->{action} eq 'issue' ) {
3907 $report = ProcessOfflineIssue( $operation );
3908 } elsif ( $operation->{action} eq 'payment' ) {
3909 $report = ProcessOfflinePayment( $operation );
3912 DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3917 sub ProcessOfflineReturn {
3918 my $operation = shift;
3920 my $item = Koha::Items->find({barcode => $operation->{barcode}});
3923 my $itemnumber = $item->itemnumber;
3924 my $issue = GetOpenIssue( $itemnumber );
3926 my $leave_item_lost = C4::Context->preference("BlockReturnOfLostItems") ? 1 : 0;
3927 ModDateLastSeen( $itemnumber, $leave_item_lost );
3929 $issue->{borrowernumber},
3931 $operation->{timestamp},
3934 $item->onloan(undef);
3935 $item->store({ log_action => 0 });
3938 return "Item not issued.";
3941 return "Item not found.";
3945 sub ProcessOfflineIssue {
3946 my $operation = shift;
3948 my $patron = Koha::Patrons->find( { cardnumber => $operation->{cardnumber} } );
3951 my $item = Koha::Items->find({ barcode => $operation->{barcode} });
3953 return "Barcode not found.";
3955 my $itemnumber = $item->itemnumber;
3956 my $issue = GetOpenIssue( $itemnumber );
3958 if ( $issue and ( $issue->{borrowernumber} ne $patron->borrowernumber ) ) { # Item already issued to another patron mark it returned
3960 $issue->{borrowernumber},
3962 $operation->{timestamp},
3967 $operation->{'barcode'},
3970 $operation->{timestamp},
3975 return "Borrower not found.";
3979 sub ProcessOfflinePayment {
3980 my $operation = shift;
3982 my $patron = Koha::Patrons->find({ cardnumber => $operation->{cardnumber} });
3984 $patron->account->pay(
3986 amount => $operation->{amount},
3987 library_id => $operation->{branchcode},
3997 TransferSlip($user_branch, $itemnumber, $barcode, $to_branch)
3999 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
4004 my ($branch, $itemnumber, $barcode, $to_branch) = @_;
4008 ? Koha::Items->find($itemnumber)
4009 : Koha::Items->find( { barcode => $barcode } );
4013 return C4::Letters::GetPreparedLetter (
4014 module => 'circulation',
4015 letter_code => 'TRANSFERSLIP',
4016 branchcode => $branch,
4018 'branches' => $to_branch,
4019 'biblio' => $item->biblionumber,
4020 'items' => $item->unblessed,
4025 =head2 CheckIfIssuedToPatron
4027 CheckIfIssuedToPatron($borrowernumber, $biblionumber)
4029 Return 1 if any record item is issued to patron, otherwise return 0
4033 sub CheckIfIssuedToPatron {
4034 my ($borrowernumber, $biblionumber) = @_;
4036 my $dbh = C4::Context->dbh;
4038 SELECT COUNT(*) FROM issues
4039 LEFT JOIN items ON items.itemnumber = issues.itemnumber
4040 WHERE items.biblionumber = ?
4041 AND issues.borrowernumber = ?
4043 my $is_issued = $dbh->selectrow_array($query, {}, $biblionumber, $borrowernumber );
4044 return 1 if $is_issued;
4050 IsItemIssued( $itemnumber )
4052 Return 1 if the item is on loan, otherwise return 0
4057 my $itemnumber = shift;
4058 my $dbh = C4::Context->dbh;
4059 my $sth = $dbh->prepare(q{
4062 WHERE itemnumber = ?
4064 $sth->execute($itemnumber);
4065 return $sth->fetchrow;
4068 =head2 GetAgeRestriction
4070 my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions, $borrower);
4071 my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions);
4073 if($daysToAgeRestriction <= 0) { #Borrower is allowed to access this material, as they are older or as old as the agerestriction }
4074 if($daysToAgeRestriction > 0) { #Borrower is this many days from meeting the agerestriction }
4076 @PARAM1 the koha.biblioitems.agerestriction value, like K18, PEGI 13, ...
4077 @PARAM2 a borrower-object with koha.borrowers.dateofbirth. (OPTIONAL)
4078 @RETURNS The age restriction age in years and the days to fulfill the age restriction for the given borrower.
4079 Negative days mean the borrower has gone past the age restriction age.
4083 sub GetAgeRestriction {
4084 my ($record_restrictions, $borrower) = @_;
4085 my $markers = C4::Context->preference('AgeRestrictionMarker');
4087 return unless $record_restrictions;
4088 # Split $record_restrictions to something like FSK 16 or PEGI 6
4089 my @values = split ' ', uc($record_restrictions);
4090 return unless @values;
4092 # Search first occurrence of one of the markers
4093 my @markers = split /\|/, uc($markers);
4094 return unless @markers;
4097 my $restriction_year = 0;
4098 for my $value (@values) {
4100 for my $marker (@markers) {
4101 $marker =~ s/^\s+//; #remove leading spaces
4102 $marker =~ s/\s+$//; #remove trailing spaces
4103 if ( $marker eq $value ) {
4104 if ( $index <= $#values ) {
4105 $restriction_year += $values[$index];
4109 elsif ( $value =~ /^\Q$marker\E(\d+)$/ ) {
4111 # Perhaps it is something like "K16" (as in Finland)
4112 $restriction_year += $1;
4116 last if ( $restriction_year > 0 );
4119 #Check if the borrower is age restricted for this material and for how long.
4120 if ($restriction_year && $borrower) {
4121 if ( $borrower->{'dateofbirth'} ) {
4122 my @alloweddate = split /-/, $borrower->{'dateofbirth'};
4123 $alloweddate[0] += $restriction_year;
4125 #Prevent runime eror on leap year (invalid date)
4126 if ( ( $alloweddate[1] == 2 ) && ( $alloweddate[2] == 29 ) ) {
4127 $alloweddate[2] = 28;
4130 #Get how many days the borrower has to reach the age restriction
4131 my @Today = split /-/, dt_from_string()->ymd();
4132 my $daysToAgeRestriction = Date_to_Days(@alloweddate) - Date_to_Days(@Today);
4133 #Negative days means the borrower went past the age restriction age
4134 return ($restriction_year, $daysToAgeRestriction);
4138 return ($restriction_year);
4142 =head2 GetPendingOnSiteCheckouts
4146 sub GetPendingOnSiteCheckouts {
4147 my $dbh = C4::Context->dbh;
4148 return $dbh->selectall_arrayref(q|
4154 items.itemcallnumber,
4158 issues.date_due < NOW() AS is_overdue,
4161 borrowers.firstname,
4163 borrowers.cardnumber,
4164 borrowers.borrowernumber
4166 LEFT JOIN issues ON items.itemnumber = issues.itemnumber
4167 LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
4168 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
4169 WHERE issues.onsite_checkout = 1
4170 |, { Slice => {} } );
4176 my ($count, $branch, $itemtype, $ccode, $newness)
4177 = @$params{qw(count branch itemtype ccode newness)};
4179 my $dbh = C4::Context->dbh;
4182 SELECT b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4183 bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4184 i.ccode, SUM(i.issues) AS count
4186 LEFT JOIN items i ON (i.biblionumber = b.biblionumber)
4187 LEFT JOIN biblioitems bi ON (bi.biblionumber = b.biblionumber)
4190 my (@where_strs, @where_args);
4193 push @where_strs, 'i.homebranch = ?';
4194 push @where_args, $branch;
4197 if (C4::Context->preference('item-level_itypes')){
4198 push @where_strs, 'i.itype = ?';
4199 push @where_args, $itemtype;
4201 push @where_strs, 'bi.itemtype = ?';
4202 push @where_args, $itemtype;
4206 push @where_strs, 'i.ccode = ?';
4207 push @where_args, $ccode;
4210 push @where_strs, 'TO_DAYS(NOW()) - TO_DAYS(b.datecreated) <= ?';
4211 push @where_args, $newness;
4215 $query .= 'WHERE ' . join(' AND ', @where_strs);
4219 GROUP BY b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4220 bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4225 $query .= q{ ) xxx WHERE count > 0 };
4226 $count = int($count);
4228 $query .= "LIMIT $count";
4231 my $rows = $dbh->selectall_arrayref($query, { Slice => {} }, @where_args);
4236 =head2 Internal methods
4240 sub _CalculateAndUpdateFine {
4243 my $borrower = $params->{borrower};
4244 my $item = $params->{item};
4245 my $issue = $params->{issue};
4246 my $return_date = $params->{return_date};
4248 unless ($borrower) { carp "No borrower passed in!" && return; }
4249 unless ($item) { carp "No item passed in!" && return; }
4250 unless ($issue) { carp "No issue passed in!" && return; }
4252 my $datedue = dt_from_string( $issue->date_due );
4254 # we only need to calculate and change the fines if we want to do that on return
4255 # Should be on for hourly loans
4256 my $control = C4::Context->preference('CircControl');
4257 my $control_branchcode =
4258 ( $control eq 'ItemHomeLibrary' ) ? $item->{homebranch}
4259 : ( $control eq 'PatronLibrary' ) ? $borrower->{branchcode}
4260 : $issue->branchcode;
4262 my $date_returned = $return_date ? $return_date : dt_from_string();
4264 my ( $amount, $unitcounttotal, $unitcount ) =
4265 C4::Overdues::CalcFine( $item, $borrower->{categorycode}, $control_branchcode, $datedue, $date_returned );
4267 if ( C4::Context->preference('finesMode') eq 'production' ) {
4268 if ( $amount > 0 ) {
4269 C4::Overdues::UpdateFine({
4270 issue_id => $issue->issue_id,
4271 itemnumber => $issue->itemnumber,
4272 borrowernumber => $issue->borrowernumber,
4274 due => output_pref($datedue),
4277 elsif ($return_date) {
4279 # Backdated returns may have fines that shouldn't exist,
4280 # so in this case, we need to drop those fines to 0
4282 C4::Overdues::UpdateFine({
4283 issue_id => $issue->issue_id,
4284 itemnumber => $issue->itemnumber,
4285 borrowernumber => $issue->borrowernumber,
4287 due => output_pref($datedue),
4293 sub _item_denied_renewal {
4296 my $item = $params->{item};
4297 return unless $item;
4299 my $denyingrules = Koha::Config::SysPrefs->find('ItemsDeniedRenewal')->get_yaml_pref_hash();
4300 return unless $denyingrules;
4301 foreach my $field (keys %$denyingrules) {
4302 my $val = $item->$field;
4303 if( !defined $val) {
4304 if ( any { !defined $_ } @{$denyingrules->{$field}} ){
4307 } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
4308 # If the results matches the values in the syspref
4309 # We return true if match found
4322 Koha Development Team <http://koha-community.org/>