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 );
32 use C4::ItemCirculationAlertPreference;
35 use C4::Log; # logaction
36 use C4::Overdues qw(CalcFine UpdateFine get_chargeable_units);
37 use C4::RotatingCollections qw(GetCollectionItemBranches);
38 use Algorithm::CheckDigits;
42 use Koha::AuthorisedValues;
43 use Koha::Biblioitems;
47 use Koha::Illrequests;
50 use Koha::Patron::Debarments;
53 use Koha::Account::Lines;
55 use Koha::Account::Lines;
56 use Koha::Account::Offsets;
57 use Koha::Config::SysPrefs;
58 use Koha::Charges::Fees;
59 use Koha::Util::SystemPreferences;
60 use Koha::Checkouts::ReturnClaims;
61 use Koha::SearchEngine::Indexer;
63 use List::MoreUtils qw( uniq any );
64 use Scalar::Util qw( looks_like_number );
75 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
81 # FIXME subs that should probably be elsewhere
86 &GetPendingOnSiteCheckouts
89 # subs to deal with issuing a book
97 &GetLatestAutoRenewDate
99 &GetBranchBorrowerCircRule
103 &CheckIfIssuedToPatron
108 # subs to deal with returns
114 # subs to deal with transfers
121 &IsBranchTransferAllowed
122 &CreateBranchTransferLimit
123 &DeleteBranchTransferLimits
127 # subs to deal with offline circulation
129 &GetOfflineOperations
132 &DeleteOfflineOperation
133 &ProcessOfflineOperation
139 C4::Circulation - Koha circulation module
147 The functions in this module deal with circulation, issues, and
148 returns, as well as general information about the library.
149 Also deals with inventory.
155 $str = &barcodedecode($barcode, [$filter]);
157 Generic filter function for barcode string.
158 Called on every circ if the System Pref itemBarcodeInputFilter is set.
159 Will do some manipulation of the barcode for systems that deliver a barcode
160 to circulation.pl that differs from the barcode stored for the item.
161 For proper functioning of this filter, calling the function on the
162 correct barcode string (items.barcode) should return an unaltered barcode.
164 The optional $filter argument is to allow for testing or explicit
165 behavior that ignores the System Pref. Valid values are the same as the
170 # FIXME -- the &decode fcn below should be wrapped into this one.
171 # FIXME -- these plugins should be moved out of Circulation.pm
174 my ($barcode, $filter) = @_;
175 my $branch = C4::Context::mybranch();
176 $filter = C4::Context->preference('itemBarcodeInputFilter') unless $filter;
177 $filter or return $barcode; # ensure filter is defined, else return untouched barcode
178 if ($filter eq 'whitespace') {
180 } elsif ($filter eq 'cuecat') {
182 my @fields = split( /\./, $barcode );
183 my @results = map( decode($_), @fields[ 1 .. $#fields ] );
184 ($#results == 2) and return $results[2];
185 } elsif ($filter eq 'T-prefix') {
186 if ($barcode =~ /^[Tt](\d)/) {
187 (defined($1) and $1 eq '0') and return $barcode;
188 $barcode = substr($barcode, 2) + 0; # FIXME: probably should be substr($barcode, 1)
190 return sprintf("T%07d", $barcode);
191 # FIXME: $barcode could be "T1", causing warning: substr outside of string
192 # Why drop the nonzero digit after the T?
193 # Why pass non-digits (or empty string) to "T%07d"?
194 } elsif ($filter eq 'libsuite8') {
195 unless($barcode =~ m/^($branch)-/i){ #if barcode starts with branch code its in Koha style. Skip it.
196 if($barcode =~ m/^(\d)/i){ #Some barcodes even start with 0's & numbers and are assumed to have b as the item type in the libsuite8 software
197 $barcode =~ s/^[0]*(\d+)$/$branch-b-$1/i;
199 $barcode =~ s/^(\D+)[0]*(\d+)$/$branch-$1-$2/i;
202 } elsif ($filter eq 'EAN13') {
203 my $ean = CheckDigits('ean');
204 if ( $ean->is_valid($barcode) ) {
205 #$barcode = sprintf('%013d',$barcode); # this doesn't work on 32-bit systems
206 $barcode = '0' x ( 13 - length($barcode) ) . $barcode;
208 warn "# [$barcode] not valid EAN-13/UPC-A\n";
211 return $barcode; # return barcode, modified or not
216 $str = &decode($chunk);
218 Decodes a segment of a string emitted by a CueCat barcode scanner and
221 FIXME: Should be replaced with Barcode::Cuecat from CPAN
222 or Javascript based decoding on the client side.
229 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-';
230 my @s = map { index( $seq, $_ ); } split( //, $encoded );
231 my $l = ( $#s + 1 ) % 4;
234 # warn "Error: Cuecat decode parsing failed!";
242 my $n = ( ( $s[0] << 6 | $s[1] ) << 6 | $s[2] ) << 6 | $s[3];
244 chr( ( $n >> 16 ) ^ 67 )
245 .chr( ( $n >> 8 & 255 ) ^ 67 )
246 .chr( ( $n & 255 ) ^ 67 );
249 $r = substr( $r, 0, length($r) - $l );
255 ($dotransfer, $messages, $iteminformation) = &transferbook({
256 from_branch => $frombranch
257 to_branch => $tobranch,
259 ignore_reserves => $ignore_reserves,
263 Transfers an item to a new branch. If the item is currently on loan, it is automatically returned before the actual transfer.
265 C<$fbr> is the code for the branch initiating the transfer.
266 C<$tbr> is the code for the branch to which the item should be transferred.
268 C<$barcode> is the barcode of the item to be transferred.
270 If C<$ignore_reserves> is true, C<&transferbook> ignores reserves.
271 Otherwise, if an item is reserved, the transfer fails.
273 C<$trigger> is the enum value for what triggered the transfer.
275 Returns three values:
281 is true if the transfer was successful.
285 is a reference-to-hash which may have any of the following keys:
291 There is no item in the catalog with the given barcode. The value is C<$barcode>.
293 =item C<DestinationEqualsHolding>
295 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.
299 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.
303 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>.
305 =item C<WasTransferred>
307 The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored.
317 my $tbr = $params->{to_branch};
318 my $fbr = $params->{from_branch};
319 my $ignoreRs = $params->{ignore_reserves};
320 my $barcode = $params->{barcode};
321 my $trigger = $params->{trigger};
324 my $item = Koha::Items->find( { barcode => $barcode } );
326 Koha::Exceptions::MissingParameter->throw(
327 "Missing mandatory parameter: from_branch")
330 Koha::Exceptions::MissingParameter->throw(
331 "Missing mandatory parameter: to_branch")
336 $messages->{'BadBarcode'} = $barcode;
338 return ( $dotransfer, $messages );
341 my $itemnumber = $item->itemnumber;
342 # get branches of book...
343 my $hbr = $item->homebranch;
345 # if using Branch Transfer Limits
346 if ( C4::Context->preference("UseBranchTransferLimits") == 1 ) {
347 my $code = C4::Context->preference("BranchTransferLimitsType") eq 'ccode' ? $item->ccode : $item->biblio->biblioitem->itemtype; # BranchTransferLimitsType is 'ccode' or 'itemtype'
348 if ( C4::Context->preference("item-level_itypes") && C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ) {
349 if ( ! IsBranchTransferAllowed( $tbr, $fbr, $item->itype ) ) {
350 $messages->{'NotAllowed'} = $tbr . "::" . $item->itype;
353 } elsif ( ! IsBranchTransferAllowed( $tbr, $fbr, $code ) ) {
354 $messages->{'NotAllowed'} = $tbr . "::" . $code;
359 # can't transfer book if is already there....
360 if ( $fbr eq $tbr ) {
361 $messages->{'DestinationEqualsHolding'} = 1;
365 # check if it is still issued to someone, return it...
366 my $issue = Koha::Checkouts->find({ itemnumber => $itemnumber });
368 AddReturn( $barcode, $fbr );
369 $messages->{'WasReturned'} = $issue->borrowernumber;
373 # That'll save a database query.
374 my ( $resfound, $resrec, undef ) =
375 CheckReserves( $itemnumber );
376 if ( $resfound and not $ignoreRs ) {
377 $resrec->{'ResFound'} = $resfound;
378 $messages->{'ResFound'} = $resrec;
382 #actually do the transfer....
384 ModItemTransfer( $itemnumber, $fbr, $tbr, $trigger );
386 # don't need to update MARC anymore, we do it in batch now
387 $messages->{'WasTransfered'} = 1;
390 ModDateLastSeen( $itemnumber );
391 return ( $dotransfer, $messages );
396 my $borrower = shift;
397 my $item_object = shift;
399 my $onsite_checkout = $params->{onsite_checkout} || 0;
400 my $switch_onsite_checkout = $params->{switch_onsite_checkout} || 0;
401 my $cat_borrower = $borrower->{'categorycode'};
402 my $dbh = C4::Context->dbh;
403 # Get which branchcode we need
404 my $branch = _GetCircControlBranch($item_object->unblessed,$borrower);
405 my $type = $item_object->effective_itemtype;
407 my ($type_object, $parent_type, $parent_maxissueqty_rule);
408 $type_object = Koha::ItemTypes->find( $type );
409 $parent_type = $type_object->parent_type if $type_object;
410 my $child_types = Koha::ItemTypes->search({ parent_type => $type });
411 # Find any children if we are a parent_type;
413 # given branch, patron category, and item type, determine
414 # applicable issuing rule
416 $parent_maxissueqty_rule = Koha::CirculationRules->get_effective_rule(
418 categorycode => $cat_borrower,
419 itemtype => $parent_type,
420 branchcode => $branch,
421 rule_name => 'maxissueqty',
424 # If the parent rule is for default type we discount it
425 $parent_maxissueqty_rule = undef if $parent_maxissueqty_rule && !defined $parent_maxissueqty_rule->itemtype;
427 my $maxissueqty_rule = Koha::CirculationRules->get_effective_rule(
429 categorycode => $cat_borrower,
431 branchcode => $branch,
432 rule_name => 'maxissueqty',
436 my $maxonsiteissueqty_rule = Koha::CirculationRules->get_effective_rule(
438 categorycode => $cat_borrower,
440 branchcode => $branch,
441 rule_name => 'maxonsiteissueqty',
446 my $patron = Koha::Patrons->find($borrower->{borrowernumber});
447 # if a rule is found and has a loan limit set, count
448 # how many loans the patron already has that meet that
450 if (defined($maxissueqty_rule) and $maxissueqty_rule->rule_value ne "") {
453 if ( $maxissueqty_rule->branchcode ) {
454 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' ) {
455 $checkouts = $patron->checkouts->search(
456 { 'me.branchcode' => $maxissueqty_rule->branchcode } );
457 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
458 $checkouts = $patron->checkouts; # if branch is the patron's home branch, then count all loans by patron
460 $checkouts = $patron->checkouts->search(
461 { 'item.homebranch' => $maxissueqty_rule->branchcode },
462 { prefetch => 'item' } );
465 $checkouts = $patron->checkouts; # if rule is not branch specific then count all loans by patron
468 my $rule_itemtype = $maxissueqty_rule->itemtype;
469 while ( my $c = $checkouts->next ) {
470 my $itemtype = $c->item->effective_itemtype;
472 unless ( $rule_itemtype ) {
473 # matching rule has the default item type, so count only
474 # those existing loans that don't fall under a more
476 @types = Koha::CirculationRules->search(
478 branchcode => $maxissueqty_rule->branchcode,
479 categorycode => [ $maxissueqty_rule->categorycode, $cat_borrower ],
480 itemtype => { '!=' => undef },
481 rule_name => 'maxissueqty'
483 )->get_column('itemtype');
485 next if grep {$_ eq $itemtype} @types;
488 if ( $parent_maxissueqty_rule ) {
489 # if we have a parent item type then we count loans of the
490 # specific item type or its siblings or parent
491 my $children = Koha::ItemTypes->search({ parent_type => $parent_type });
492 @types = $children->get_column('itemtype');
493 push @types, $parent_type;
494 } elsif ( $child_types ) {
495 # If we are a parent type, we need to count all child types and our own type
496 @types = $child_types->get_column('itemtype');
497 push @types, $type; # And don't forget to count our own types
498 } else { push @types, $type; } # Otherwise only count the specific itemtype
500 next unless grep {$_ eq $itemtype} @types;
502 $sum_checkouts->{total}++;
503 $sum_checkouts->{onsite_checkouts}++ if $c->onsite_checkout;
504 $sum_checkouts->{itemtype}->{$itemtype}++;
507 my $checkout_count_type = $sum_checkouts->{itemtype}->{$type} || 0;
508 my $checkout_count = $sum_checkouts->{total} || 0;
509 my $onsite_checkout_count = $sum_checkouts->{onsite_checkouts} || 0;
511 my $checkout_rules = {
512 checkout_count => $checkout_count,
513 onsite_checkout_count => $onsite_checkout_count,
514 onsite_checkout => $onsite_checkout,
515 max_checkouts_allowed => $maxissueqty_rule ? $maxissueqty_rule->rule_value : undef,
516 max_onsite_checkouts_allowed => $maxonsiteissueqty_rule ? $maxonsiteissueqty_rule->rule_value : undef,
517 switch_onsite_checkout => $switch_onsite_checkout,
519 # If parent rules exists
520 if ( defined($parent_maxissueqty_rule) and defined($parent_maxissueqty_rule->rule_value) ){
521 $checkout_rules->{max_checkouts_allowed} = $parent_maxissueqty_rule ? $parent_maxissueqty_rule->rule_value : undef;
522 my $qty_over = _check_max_qty($checkout_rules);
523 return $qty_over if defined $qty_over;
525 # If the parent rule is less than or equal to the child, we only need check the parent
526 if( $maxissueqty_rule->rule_value < $parent_maxissueqty_rule->rule_value && defined($maxissueqty_rule->itemtype) ) {
527 $checkout_rules->{checkout_count} = $checkout_count_type;
528 $checkout_rules->{max_checkouts_allowed} = $maxissueqty_rule ? $maxissueqty_rule->rule_value : undef;
529 my $qty_over = _check_max_qty($checkout_rules);
530 return $qty_over if defined $qty_over;
533 my $qty_over = _check_max_qty($checkout_rules);
534 return $qty_over if defined $qty_over;
538 # Now count total loans against the limit for the branch
539 my $branch_borrower_circ_rule = GetBranchBorrowerCircRule($branch, $cat_borrower);
540 if (defined($branch_borrower_circ_rule->{patron_maxissueqty}) and $branch_borrower_circ_rule->{patron_maxissueqty} ne '') {
542 if ( C4::Context->preference('CircControl') eq 'PickupLibrary' ) {
543 $checkouts = $patron->checkouts->search(
544 { 'me.branchcode' => $branch} );
545 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
546 $checkouts = $patron->checkouts; # if branch is the patron's home branch, then count all loans by patron
548 $checkouts = $patron->checkouts->search(
549 { 'item.homebranch' => $branch},
550 { prefetch => 'item' } );
553 my $checkout_count = $checkouts->count;
554 my $onsite_checkout_count = $checkouts->search({ onsite_checkout => 1 })->count;
555 my $max_checkouts_allowed = $branch_borrower_circ_rule->{patron_maxissueqty};
556 my $max_onsite_checkouts_allowed = $branch_borrower_circ_rule->{patron_maxonsiteissueqty} || undef;
558 my $qty_over = _check_max_qty(
560 checkout_count => $checkout_count,
561 onsite_checkout_count => $onsite_checkout_count,
562 onsite_checkout => $onsite_checkout,
563 max_checkouts_allowed => $max_checkouts_allowed,
564 max_onsite_checkouts_allowed => $max_onsite_checkouts_allowed,
565 switch_onsite_checkout => $switch_onsite_checkout
568 return $qty_over if defined $qty_over;
571 if ( not defined( $maxissueqty_rule ) and not defined($branch_borrower_circ_rule->{patron_maxissueqty}) ) {
572 return { reason => 'NO_RULE_DEFINED', max_allowed => 0 };
575 # OK, the patron can issue !!!
581 my $checkout_count = $params->{checkout_count};
582 my $onsite_checkout_count = $params->{onsite_checkout_count};
583 my $onsite_checkout = $params->{onsite_checkout};
584 my $max_checkouts_allowed = $params->{max_checkouts_allowed};
585 my $max_onsite_checkouts_allowed = $params->{max_onsite_checkouts_allowed};
586 my $switch_onsite_checkout = $params->{switch_onsite_checkout};
588 if ( $onsite_checkout and defined $max_onsite_checkouts_allowed ) {
589 if ( $max_onsite_checkouts_allowed eq '' ) { return; }
590 if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed ) {
592 reason => 'TOO_MANY_ONSITE_CHECKOUTS',
593 count => $onsite_checkout_count,
594 max_allowed => $max_onsite_checkouts_allowed,
598 if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) {
599 if ( $max_checkouts_allowed eq '' ) { return; }
600 my $delta = $switch_onsite_checkout ? 1 : 0;
601 if ( $checkout_count >= $max_checkouts_allowed + $delta ) {
603 reason => 'TOO_MANY_CHECKOUTS',
604 count => $checkout_count,
605 max_allowed => $max_checkouts_allowed,
609 elsif ( not $onsite_checkout ) {
610 if ( $max_checkouts_allowed eq '' ) { return; }
612 $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed )
615 reason => 'TOO_MANY_CHECKOUTS',
616 count => $checkout_count - $onsite_checkout_count,
617 max_allowed => $max_checkouts_allowed,
625 =head2 CanBookBeIssued
627 ( $issuingimpossible, $needsconfirmation, [ $alerts ] ) = CanBookBeIssued( $patron,
628 $barcode, $duedate, $inprocess, $ignore_reserves, $params );
630 Check if a book can be issued.
632 C<$issuingimpossible> and C<$needsconfirmation> are hashrefs.
634 IMPORTANT: The assumption by users of this routine is that causes blocking
635 the issue are keyed by uppercase labels and other returned
636 data is keyed in lower case!
640 =item C<$patron> is a Koha::Patron
642 =item C<$barcode> is the bar code of the book being issued.
644 =item C<$duedates> is a DateTime object.
646 =item C<$inprocess> boolean switch
648 =item C<$ignore_reserves> boolean switch
650 =item C<$params> Hashref of additional parameters
653 override_high_holds - Ignore high holds
654 onsite_checkout - Checkout is an onsite checkout that will not leave the library
662 =item C<$issuingimpossible> a reference to a hash. It contains reasons why issuing is impossible.
663 Possible values are :
669 sticky due date is invalid
673 borrower gone with no address
677 borrower declared it's card lost
683 =head3 UNKNOWN_BARCODE
697 item is restricted (set by ??)
699 C<$needsconfirmation> a reference to a hash. It contains reasons why the loan
700 could be prevented, but ones that can be overriden by the operator.
702 Possible values are :
710 renewing, not issuing
712 =head3 ISSUED_TO_ANOTHER
714 issued to someone else.
718 reserved for someone else.
722 sticky due date is invalid or due date in the past
726 if the borrower borrows to much things
730 sub CanBookBeIssued {
731 my ( $patron, $barcode, $duedate, $inprocess, $ignore_reserves, $params ) = @_;
732 my %needsconfirmation; # filled with problems that needs confirmations
733 my %issuingimpossible; # filled with problems that causes the issue to be IMPOSSIBLE
734 my %alerts; # filled with messages that shouldn't stop issuing, but the librarian should be aware of.
735 my %messages; # filled with information messages that should be displayed.
737 my $onsite_checkout = $params->{onsite_checkout} || 0;
738 my $override_high_holds = $params->{override_high_holds} || 0;
740 my $item_object = Koha::Items->find({barcode => $barcode });
742 # MANDATORY CHECKS - unless item exists, nothing else matters
743 unless ( $item_object ) {
744 $issuingimpossible{UNKNOWN_BARCODE} = 1;
746 return ( \%issuingimpossible, \%needsconfirmation ) if %issuingimpossible;
748 my $item_unblessed = $item_object->unblessed; # Transition...
749 my $issue = $item_object->checkout;
750 my $biblio = $item_object->biblio;
752 my $biblioitem = $biblio->biblioitem;
753 my $effective_itemtype = $item_object->effective_itemtype;
754 my $dbh = C4::Context->dbh;
755 my $patron_unblessed = $patron->unblessed;
757 my $circ_library = Koha::Libraries->find( _GetCircControlBranch($item_unblessed, $patron_unblessed) );
759 # DUE DATE is OK ? -- should already have checked.
761 if ($duedate && ref $duedate ne 'DateTime') {
762 $duedate = dt_from_string($duedate);
764 my $now = dt_from_string();
765 unless ( $duedate ) {
766 my $issuedate = $now->clone();
768 $duedate = CalcDateDue( $issuedate, $effective_itemtype, $circ_library->branchcode, $patron_unblessed );
770 # Offline circ calls AddIssue directly, doesn't run through here
771 # So issuingimpossible should be ok.
774 my $fees = Koha::Charges::Fees->new(
777 library => $circ_library,
778 item => $item_object,
784 my $today = $now->clone();
785 $today->truncate( to => 'minute');
786 if (DateTime->compare($duedate,$today) == -1 ) { # duedate cannot be before now
787 $needsconfirmation{INVALID_DATE} = output_pref($duedate);
790 $issuingimpossible{INVALID_DATE} = output_pref($duedate);
796 if ( $patron->category->category_type eq 'X' && ( $item_object->barcode )) {
797 # stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1 .
799 branch => C4::Context->userenv->{'branch'},
801 itemnumber => $item_object->itemnumber,
802 itemtype => $effective_itemtype,
803 borrowernumber => $patron->borrowernumber,
804 ccode => $item_object->ccode}
806 ModDateLastSeen( $item_object->itemnumber ); # FIXME Move to Koha::Item
807 return( { STATS => 1 }, {});
810 if ( $patron->gonenoaddress && $patron->gonenoaddress == 1 ) {
811 $issuingimpossible{GNA} = 1;
814 if ( $patron->lost && $patron->lost == 1 ) {
815 $issuingimpossible{CARD_LOST} = 1;
817 if ( $patron->is_debarred ) {
818 $issuingimpossible{DEBARRED} = 1;
821 if ( $patron->is_expired ) {
822 $issuingimpossible{EXPIRED} = 1;
830 my $account = $patron->account;
831 my $balance = $account->balance;
832 my $non_issues_charges = $account->non_issues_charges;
833 my $other_charges = $balance - $non_issues_charges;
835 my $amountlimit = C4::Context->preference("noissuescharge");
836 my $allowfineoverride = C4::Context->preference("AllowFineOverride");
837 my $allfinesneedoverride = C4::Context->preference("AllFinesNeedOverride");
839 # Check the debt of this patrons guarantees
840 my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
841 $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
842 if ( defined $no_issues_charge_guarantees ) {
843 my @guarantees = map { $_->guarantee } $patron->guarantee_relationships();
844 my $guarantees_non_issues_charges;
845 foreach my $g ( @guarantees ) {
846 $guarantees_non_issues_charges += $g->account->non_issues_charges;
849 if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && !$allowfineoverride) {
850 $issuingimpossible{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
851 } elsif ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && $allowfineoverride) {
852 $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
853 } elsif ( $allfinesneedoverride && $guarantees_non_issues_charges > 0 && $guarantees_non_issues_charges <= $no_issues_charge_guarantees && !$inprocess ) {
854 $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
858 # Check the debt of this patrons guarantors *and* the guarantees of those guarantors
859 my $no_issues_charge_guarantors = C4::Context->preference("NoIssuesChargeGuarantorsWithGuarantees");
860 $no_issues_charge_guarantors = undef unless looks_like_number( $no_issues_charge_guarantors );
861 if ( defined $no_issues_charge_guarantors ) {
862 my $guarantors_non_issues_charges += $patron->relationships_debt({ include_guarantors => 1, only_this_guarantor => 0, include_this_patron => 1 });
864 if ( $guarantors_non_issues_charges > $no_issues_charge_guarantors && !$inprocess && !$allowfineoverride) {
865 $issuingimpossible{DEBT_GUARANTORS} = $guarantors_non_issues_charges;
866 } elsif ( $guarantors_non_issues_charges > $no_issues_charge_guarantors && !$inprocess && $allowfineoverride) {
867 $needsconfirmation{DEBT_GUARANTORS} = $guarantors_non_issues_charges;
868 } elsif ( $allfinesneedoverride && $guarantors_non_issues_charges > 0 && $guarantors_non_issues_charges <= $no_issues_charge_guarantors && !$inprocess ) {
869 $needsconfirmation{DEBT_GUARANTORS} = $guarantors_non_issues_charges;
873 if ( C4::Context->preference("IssuingInProcess") ) {
874 if ( $non_issues_charges > $amountlimit && !$inprocess && !$allowfineoverride) {
875 $issuingimpossible{DEBT} = $non_issues_charges;
876 } elsif ( $non_issues_charges > $amountlimit && !$inprocess && $allowfineoverride) {
877 $needsconfirmation{DEBT} = $non_issues_charges;
878 } elsif ( $allfinesneedoverride && $non_issues_charges > 0 && $non_issues_charges <= $amountlimit && !$inprocess ) {
879 $needsconfirmation{DEBT} = $non_issues_charges;
883 if ( $non_issues_charges > $amountlimit && $allowfineoverride ) {
884 $needsconfirmation{DEBT} = $non_issues_charges;
885 } elsif ( $non_issues_charges > $amountlimit && !$allowfineoverride) {
886 $issuingimpossible{DEBT} = $non_issues_charges;
887 } elsif ( $non_issues_charges > 0 && $allfinesneedoverride ) {
888 $needsconfirmation{DEBT} = $non_issues_charges;
892 if ($balance > 0 && $other_charges > 0) {
893 $alerts{OTHER_CHARGES} = sprintf( "%.2f", $other_charges );
896 $patron = Koha::Patrons->find( $patron->borrowernumber ); # FIXME Refetch just in case, to avoid regressions. But must not be needed
897 $patron_unblessed = $patron->unblessed;
899 if ( my $debarred_date = $patron->is_debarred ) {
900 # patron has accrued fine days or has a restriction. $count is a date
901 if ($debarred_date eq '9999-12-31') {
902 $issuingimpossible{USERBLOCKEDNOENDDATE} = $debarred_date;
905 $issuingimpossible{USERBLOCKEDWITHENDDATE} = $debarred_date;
907 } elsif ( my $num_overdues = $patron->has_overdues ) {
908 ## patron has outstanding overdue loans
909 if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){
910 $issuingimpossible{USERBLOCKEDOVERDUE} = $num_overdues;
912 elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){
913 $needsconfirmation{USERBLOCKEDOVERDUE} = $num_overdues;
917 # Additional Materials Check
918 if ( C4::Context->preference("CircConfirmItemParts")
919 && $item_object->materials )
921 $needsconfirmation{ADDITIONAL_MATERIALS} = $item_object->materials;
925 # CHECK IF BOOK ALREADY ISSUED TO THIS BORROWER
927 if ( $issue && $issue->borrowernumber eq $patron->borrowernumber ){
929 # Already issued to current borrower.
930 # If it is an on-site checkout if it can be switched to a normal checkout
931 # or ask whether the loan should be renewed
933 if ( $issue->onsite_checkout
934 and C4::Context->preference('SwitchOnSiteCheckouts') ) {
935 $messages{ONSITE_CHECKOUT_WILL_BE_SWITCHED} = 1;
937 my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed(
938 $patron->borrowernumber,
939 $item_object->itemnumber,
941 if ( $CanBookBeRenewed == 0 ) { # no more renewals allowed
942 if ( $renewerror eq 'onsite_checkout' ) {
943 $issuingimpossible{NO_RENEWAL_FOR_ONSITE_CHECKOUTS} = 1;
946 $issuingimpossible{NO_MORE_RENEWALS} = 1;
950 $needsconfirmation{RENEW_ISSUE} = 1;
956 # issued to someone else
958 my $patron = Koha::Patrons->find( $issue->borrowernumber );
960 my ( $can_be_returned, $message ) = CanBookBeReturned( $item_unblessed, C4::Context->userenv->{branch} );
962 unless ( $can_be_returned ) {
963 $issuingimpossible{RETURN_IMPOSSIBLE} = 1;
964 $issuingimpossible{branch_to_return} = $message;
966 if ( C4::Context->preference('AutoReturnCheckedOutItems') ) {
967 $alerts{RETURNED_FROM_ANOTHER} = { patron => $patron };
969 $needsconfirmation{ISSUED_TO_ANOTHER} = 1;
970 $needsconfirmation{issued_firstname} = $patron->firstname;
971 $needsconfirmation{issued_surname} = $patron->surname;
972 $needsconfirmation{issued_cardnumber} = $patron->cardnumber;
973 $needsconfirmation{issued_borrowernumber} = $patron->borrowernumber;
978 # JB34 CHECKS IF BORROWERS DON'T HAVE ISSUE TOO MANY BOOKS
980 my $switch_onsite_checkout = (
981 C4::Context->preference('SwitchOnSiteCheckouts')
983 and $issue->onsite_checkout
984 and $issue->borrowernumber == $patron->borrowernumber ? 1 : 0 );
985 my $toomany = TooMany( $patron_unblessed, $item_object, { onsite_checkout => $onsite_checkout, switch_onsite_checkout => $switch_onsite_checkout, } );
986 # if TooMany max_allowed returns 0 the user doesn't have permission to check out this book
987 if ( $toomany && not exists $needsconfirmation{RENEW_ISSUE} ) {
988 if ( $toomany->{max_allowed} == 0 ) {
989 $needsconfirmation{PATRON_CANT} = 1;
991 if ( C4::Context->preference("AllowTooManyOverride") ) {
992 $needsconfirmation{TOO_MANY} = $toomany->{reason};
993 $needsconfirmation{current_loan_count} = $toomany->{count};
994 $needsconfirmation{max_loans_allowed} = $toomany->{max_allowed};
996 $issuingimpossible{TOO_MANY} = $toomany->{reason};
997 $issuingimpossible{current_loan_count} = $toomany->{count};
998 $issuingimpossible{max_loans_allowed} = $toomany->{max_allowed};
1003 # CHECKPREVCHECKOUT: CHECK IF ITEM HAS EVER BEEN LENT TO PATRON
1005 $patron = Koha::Patrons->find( $patron->borrowernumber ); # FIXME Refetch just in case, to avoid regressions. But must not be needed
1006 my $wants_check = $patron->wants_check_for_previous_checkout;
1007 $needsconfirmation{PREVISSUE} = 1
1008 if ($wants_check and $patron->do_check_for_previous_checkout($item_unblessed));
1013 if ( $item_object->notforloan )
1015 if(!C4::Context->preference("AllowNotForLoanOverride")){
1016 $issuingimpossible{NOT_FOR_LOAN} = 1;
1017 $issuingimpossible{item_notforloan} = $item_object->notforloan;
1019 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
1020 $needsconfirmation{item_notforloan} = $item_object->notforloan;
1024 # we have to check itemtypes.notforloan also
1025 if (C4::Context->preference('item-level_itypes')){
1026 # this should probably be a subroutine
1027 my $sth = $dbh->prepare("SELECT notforloan FROM itemtypes WHERE itemtype = ?");
1028 $sth->execute($effective_itemtype);
1029 my $notforloan=$sth->fetchrow_hashref();
1030 if ($notforloan->{'notforloan'}) {
1031 if (!C4::Context->preference("AllowNotForLoanOverride")) {
1032 $issuingimpossible{NOT_FOR_LOAN} = 1;
1033 $issuingimpossible{itemtype_notforloan} = $effective_itemtype;
1035 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
1036 $needsconfirmation{itemtype_notforloan} = $effective_itemtype;
1041 my $itemtype = Koha::ItemTypes->find($biblioitem->itemtype);
1042 if ( $itemtype && defined $itemtype->notforloan && $itemtype->notforloan == 1){
1043 if (!C4::Context->preference("AllowNotForLoanOverride")) {
1044 $issuingimpossible{NOT_FOR_LOAN} = 1;
1045 $issuingimpossible{itemtype_notforloan} = $effective_itemtype;
1047 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
1048 $needsconfirmation{itemtype_notforloan} = $effective_itemtype;
1053 if ( $item_object->withdrawn && $item_object->withdrawn > 0 )
1055 $issuingimpossible{WTHDRAWN} = 1;
1057 if ( $item_object->restricted
1058 && $item_object->restricted == 1 )
1060 $issuingimpossible{RESTRICTED} = 1;
1062 if ( $item_object->itemlost && C4::Context->preference("IssueLostItem") ne 'nothing' ) {
1063 my $av = Koha::AuthorisedValues->search({ category => 'LOST', authorised_value => $item_object->itemlost });
1064 my $code = $av->count ? $av->next->lib : '';
1065 $needsconfirmation{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'confirm' );
1066 $alerts{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'alert' );
1068 if ( C4::Context->preference("IndependentBranches") ) {
1069 my $userenv = C4::Context->userenv;
1070 unless ( C4::Context->IsSuperLibrarian() ) {
1071 my $HomeOrHoldingBranch = C4::Context->preference("HomeOrHoldingBranch");
1072 if ( $item_object->$HomeOrHoldingBranch ne $userenv->{branch} ){
1073 $issuingimpossible{ITEMNOTSAMEBRANCH} = 1;
1074 $issuingimpossible{'itemhomebranch'} = $item_object->$HomeOrHoldingBranch;
1076 $needsconfirmation{BORRNOTSAMEBRANCH} = $patron->branchcode
1077 if ( $patron->branchcode ne $userenv->{branch} );
1082 # CHECK IF THERE IS RENTAL CHARGES. RENTAL MUST BE CONFIRMED BY THE BORROWER
1084 my $rentalConfirmation = C4::Context->preference("RentalFeesCheckoutConfirmation");
1085 if ($rentalConfirmation) {
1086 my ($rentalCharge) = GetIssuingCharges( $item_object->itemnumber, $patron->borrowernumber );
1088 my $itemtype_object = Koha::ItemTypes->find( $item_object->effective_itemtype );
1089 if ($itemtype_object) {
1090 my $accumulate_charge = $fees->accumulate_rentalcharge();
1091 if ( $accumulate_charge > 0 ) {
1092 $rentalCharge += $accumulate_charge;
1096 if ( $rentalCharge > 0 ) {
1097 $needsconfirmation{RENTALCHARGE} = $rentalCharge;
1101 unless ( $ignore_reserves ) {
1102 # See if the item is on reserve.
1103 my ( $restype, $res ) = C4::Reserves::CheckReserves( $item_object->itemnumber );
1105 my $resbor = $res->{'borrowernumber'};
1106 if ( $resbor ne $patron->borrowernumber ) {
1107 my $patron = Koha::Patrons->find( $resbor );
1108 if ( $restype eq "Waiting" )
1110 # The item is on reserve and waiting, but has been
1111 # reserved by some other patron.
1112 $needsconfirmation{RESERVE_WAITING} = 1;
1113 $needsconfirmation{'resfirstname'} = $patron->firstname;
1114 $needsconfirmation{'ressurname'} = $patron->surname;
1115 $needsconfirmation{'rescardnumber'} = $patron->cardnumber;
1116 $needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
1117 $needsconfirmation{'resbranchcode'} = $res->{branchcode};
1118 $needsconfirmation{'reswaitingdate'} = $res->{'waitingdate'};
1119 $needsconfirmation{'reserve_id'} = $res->{reserve_id};
1121 elsif ( $restype eq "Reserved" ) {
1122 # The item is on reserve for someone else.
1123 $needsconfirmation{RESERVED} = 1;
1124 $needsconfirmation{'resfirstname'} = $patron->firstname;
1125 $needsconfirmation{'ressurname'} = $patron->surname;
1126 $needsconfirmation{'rescardnumber'} = $patron->cardnumber;
1127 $needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
1128 $needsconfirmation{'resbranchcode'} = $patron->branchcode;
1129 $needsconfirmation{'resreservedate'} = $res->{reservedate};
1130 $needsconfirmation{'reserve_id'} = $res->{reserve_id};
1136 ## CHECK AGE RESTRICTION
1137 my $agerestriction = $biblioitem->agerestriction;
1138 my ($restriction_age, $daysToAgeRestriction) = GetAgeRestriction( $agerestriction, $patron->unblessed );
1139 if ( $daysToAgeRestriction && $daysToAgeRestriction > 0 ) {
1140 if ( C4::Context->preference('AgeRestrictionOverride') ) {
1141 $needsconfirmation{AGE_RESTRICTION} = "$agerestriction";
1144 $issuingimpossible{AGE_RESTRICTION} = "$agerestriction";
1148 ## check for high holds decreasing loan period
1149 if ( C4::Context->preference('decreaseLoanHighHolds') ) {
1150 my $check = checkHighHolds( $item_unblessed, $patron_unblessed );
1152 if ( $check->{exceeded} ) {
1153 if ($override_high_holds) {
1154 $alerts{HIGHHOLDS} = {
1155 num_holds => $check->{outstanding},
1156 duration => $check->{duration},
1157 returndate => output_pref( { dt => dt_from_string($check->{due_date}), dateformat => 'iso', timeformat => '24hr' }),
1161 $needsconfirmation{HIGHHOLDS} = {
1162 num_holds => $check->{outstanding},
1163 duration => $check->{duration},
1164 returndate => output_pref( { dt => dt_from_string($check->{due_date}), dateformat => 'iso', timeformat => '24hr' }),
1171 !C4::Context->preference('AllowMultipleIssuesOnABiblio') &&
1172 # don't do the multiple loans per bib check if we've
1173 # already determined that we've got a loan on the same item
1174 !$issuingimpossible{NO_MORE_RENEWALS} &&
1175 !$needsconfirmation{RENEW_ISSUE}
1177 # Check if borrower has already issued an item from the same biblio
1178 # Only if it's not a subscription
1179 my $biblionumber = $item_object->biblionumber;
1180 require C4::Serials;
1181 my $is_a_subscription = C4::Serials::CountSubscriptionFromBiblionumber($biblionumber);
1182 unless ($is_a_subscription) {
1183 # FIXME Should be $patron->checkouts($args);
1184 my $checkouts = Koha::Checkouts->search(
1186 borrowernumber => $patron->borrowernumber,
1187 biblionumber => $biblionumber,
1193 # if we get here, we don't already have a loan on this item,
1194 # so if there are any loans on this bib, ask for confirmation
1195 if ( $checkouts->count ) {
1196 $needsconfirmation{BIBLIO_ALREADY_ISSUED} = 1;
1201 return ( \%issuingimpossible, \%needsconfirmation, \%alerts, \%messages, );
1204 =head2 CanBookBeReturned
1206 ($returnallowed, $message) = CanBookBeReturned($item, $branch)
1208 Check whether the item can be returned to the provided branch
1212 =item C<$item> is a hash of item information as returned Koha::Items->find->unblessed (Temporary, should be a Koha::Item instead)
1214 =item C<$branch> is the branchcode where the return is taking place
1222 =item C<$returnallowed> is 0 or 1, corresponding to whether the return is allowed (1) or not (0)
1224 =item C<$message> is the branchcode where the item SHOULD be returned, if the return is not allowed
1230 sub CanBookBeReturned {
1231 my ($item, $branch) = @_;
1232 my $allowreturntobranch = C4::Context->preference("AllowReturnToBranch") || 'anywhere';
1234 # assume return is allowed to start
1238 # identify all cases where return is forbidden
1239 if ($allowreturntobranch eq 'homebranch' && $branch ne $item->{'homebranch'}) {
1241 $message = $item->{'homebranch'};
1242 } elsif ($allowreturntobranch eq 'holdingbranch' && $branch ne $item->{'holdingbranch'}) {
1244 $message = $item->{'holdingbranch'};
1245 } elsif ($allowreturntobranch eq 'homeorholdingbranch' && $branch ne $item->{'homebranch'} && $branch ne $item->{'holdingbranch'}) {
1247 $message = $item->{'homebranch'}; # FIXME: choice of homebranch is arbitrary
1250 return ($allowed, $message);
1253 =head2 CheckHighHolds
1255 used when syspref decreaseLoanHighHolds is active. Returns 1 or 0 to define whether the minimum value held in
1256 decreaseLoanHighHoldsValue is exceeded, the total number of outstanding holds, the number of days the loan
1257 has been decreased to (held in syspref decreaseLoanHighHoldsValue), and the new due date
1261 sub checkHighHolds {
1262 my ( $item, $borrower ) = @_;
1263 my $branchcode = _GetCircControlBranch( $item, $borrower );
1264 my $item_object = Koha::Items->find( $item->{itemnumber} );
1273 my $holds = Koha::Holds->search( { biblionumber => $item->{'biblionumber'} } );
1275 if ( $holds->count() ) {
1276 $return_data->{outstanding} = $holds->count();
1278 my $decreaseLoanHighHoldsControl = C4::Context->preference('decreaseLoanHighHoldsControl');
1279 my $decreaseLoanHighHoldsValue = C4::Context->preference('decreaseLoanHighHoldsValue');
1280 my $decreaseLoanHighHoldsIgnoreStatuses = C4::Context->preference('decreaseLoanHighHoldsIgnoreStatuses');
1282 my @decreaseLoanHighHoldsIgnoreStatuses = split( /,/, $decreaseLoanHighHoldsIgnoreStatuses );
1284 if ( $decreaseLoanHighHoldsControl eq 'static' ) {
1286 # static means just more than a given number of holds on the record
1288 # If the number of holds is less than the threshold, we can stop here
1289 if ( $holds->count() < $decreaseLoanHighHoldsValue ) {
1290 return $return_data;
1293 elsif ( $decreaseLoanHighHoldsControl eq 'dynamic' ) {
1295 # dynamic means X more than the number of holdable items on the record
1297 # let's get the items
1298 my @items = $holds->next()->biblio()->items()->as_list;
1300 # Remove any items with status defined to be ignored even if the would not make item unholdable
1301 foreach my $status (@decreaseLoanHighHoldsIgnoreStatuses) {
1302 @items = grep { !$_->$status } @items;
1305 # Remove any items that are not holdable for this patron
1306 @items = grep { CanItemBeReserved( $borrower->{borrowernumber}, $_->itemnumber, undef, { ignore_found_holds => 1 } )->{status} eq 'OK' } @items;
1308 my $items_count = scalar @items;
1310 my $threshold = $items_count + $decreaseLoanHighHoldsValue;
1312 # If the number of holds is less than the count of items we have
1313 # plus the number of holds allowed above that count, we can stop here
1314 if ( $holds->count() <= $threshold ) {
1315 return $return_data;
1319 my $issuedate = dt_from_string();
1321 my $itype = $item_object->effective_itemtype;
1322 my $daysmode = Koha::CirculationRules->get_effective_daysmode(
1324 categorycode => $borrower->{categorycode},
1326 branchcode => $branchcode,
1329 my $calendar = Koha::Calendar->new( branchcode => $branchcode, days_mode => $daysmode );
1331 my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branchcode, $borrower );
1333 my $decreaseLoanHighHoldsDuration = C4::Context->preference('decreaseLoanHighHoldsDuration');
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',
1342 my $reduced_datedue;
1344 if ( defined($rule) && $rule->rule_value ne '' ){
1345 # overrides decreaseLoanHighHoldsDuration syspref
1346 $duration = $rule->rule_value;
1347 $reduced_datedue = $calendar->addDate( $issuedate, $rule->rule_value );
1349 $duration = $decreaseLoanHighHoldsDuration;
1350 $reduced_datedue = $calendar->addDate( $issuedate, $decreaseLoanHighHoldsDuration );
1352 $reduced_datedue->set_hour($orig_due->hour);
1353 $reduced_datedue->set_minute($orig_due->minute);
1354 $reduced_datedue->truncate( to => 'minute' );
1356 if ( DateTime->compare( $reduced_datedue, $orig_due ) == -1 ) {
1357 $return_data->{exceeded} = 1;
1358 $return_data->{duration} = $duration;
1359 $return_data->{due_date} = $reduced_datedue;
1363 return $return_data;
1368 &AddIssue($borrower, $barcode, [$datedue], [$cancelreserve], [$issuedate])
1370 Issue a book. Does no check, they are done in CanBookBeIssued. If we reach this sub, it means the user confirmed if needed.
1374 =item C<$borrower> is a hash with borrower informations (from Koha::Patron->unblessed).
1376 =item C<$barcode> is the barcode of the item being issued.
1378 =item C<$datedue> is a DateTime object for the max date of return, i.e. the date due (optional).
1379 Calculated if empty.
1381 =item C<$cancelreserve> is 1 to override and cancel any pending reserves for the item (optional).
1383 =item C<$issuedate> is the date to issue the item in iso (YYYY-MM-DD) format (optional).
1384 Defaults to today. Unlike C<$datedue>, NOT a DateTime object, unfortunately.
1386 AddIssue does the following things :
1388 - step 01: check that there is a borrowernumber & a barcode provided
1389 - check for RENEWAL (book issued & being issued to the same patron)
1390 - renewal YES = Calculate Charge & renew
1392 * BOOK ACTUALLY ISSUED ? do a return if book is actually issued (but to someone else)
1394 - fill reserve if reserve to this patron
1395 - cancel reserve or not, otherwise
1396 * TRANSFERT PENDING ?
1397 - complete the transfert
1405 my ( $borrower, $barcode, $datedue, $cancelreserve, $issuedate, $sipmode, $params ) = @_;
1407 my $onsite_checkout = $params && $params->{onsite_checkout} ? 1 : 0;
1408 my $switch_onsite_checkout = $params && $params->{switch_onsite_checkout};
1409 my $auto_renew = $params && $params->{auto_renew};
1410 my $dbh = C4::Context->dbh;
1411 my $barcodecheck = CheckValidBarcode($barcode);
1415 if ( $datedue && ref $datedue ne 'DateTime' ) {
1416 $datedue = dt_from_string($datedue);
1419 # $issuedate defaults to today.
1420 if ( !defined $issuedate ) {
1421 $issuedate = dt_from_string();
1424 if ( ref $issuedate ne 'DateTime' ) {
1425 $issuedate = dt_from_string($issuedate);
1430 # Stop here if the patron or barcode doesn't exist
1431 if ( $borrower && $barcode && $barcodecheck ) {
1432 # find which item we issue
1433 my $item_object = Koha::Items->find({ barcode => $barcode })
1434 or return; # if we don't get an Item, abort.
1435 my $item_unblessed = $item_object->unblessed;
1437 my $branchcode = _GetCircControlBranch( $item_unblessed, $borrower );
1439 # get actual issuing if there is one
1440 my $actualissue = $item_object->checkout;
1442 # check if we just renew the issue.
1443 if ( $actualissue and $actualissue->borrowernumber eq $borrower->{'borrowernumber'}
1444 and not $switch_onsite_checkout ) {
1445 $datedue = AddRenewal(
1446 $borrower->{'borrowernumber'},
1447 $item_object->itemnumber,
1450 $issuedate, # here interpreted as the renewal date
1455 my $itype = $item_object->effective_itemtype;
1456 $datedue = CalcDateDue( $issuedate, $itype, $branchcode, $borrower );
1459 $datedue->truncate( to => 'minute' );
1461 my $patron = Koha::Patrons->find( $borrower );
1462 my $library = Koha::Libraries->find( $branchcode );
1463 my $fees = Koha::Charges::Fees->new(
1466 library => $library,
1467 item => $item_object,
1468 to_date => $datedue,
1472 # it's NOT a renewal
1473 if ( $actualissue and not $switch_onsite_checkout ) {
1474 # This book is currently on loan, but not to the person
1475 # who wants to borrow it now. mark it returned before issuing to the new borrower
1476 my ( $allowed, $message ) = CanBookBeReturned( $item_unblessed, C4::Context->userenv->{branch} );
1477 return unless $allowed;
1478 AddReturn( $item_object->barcode, C4::Context->userenv->{'branch'} );
1481 C4::Reserves::MoveReserve( $item_object->itemnumber, $borrower->{'borrowernumber'}, $cancelreserve );
1483 # Starting process for transfer job (checking transfert and validate it if we have one)
1484 if ( my $transfer = $item_object->get_transfer ) {
1485 # updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....)
1488 datearrived => dt_from_string,
1489 tobranch => C4::Context->userenv->{branch},
1490 comments => 'Forced branchtransfer'
1493 if ( $transfer->reason && $transfer->reason eq 'Reserve' ) {
1494 my $hold = $item_object->holds->search( { found => 'T' } )->next;
1495 if ( $hold ) { # Is this really needed?
1496 $hold->set( { found => undef } )->store;
1497 C4::Reserves::ModReserveMinusPriority($item_object->itemnumber, $hold->reserve_id);
1502 # If automatic renewal wasn't selected while issuing, set the value according to the issuing rule.
1503 unless ($auto_renew) {
1504 my $rule = Koha::CirculationRules->get_effective_rule(
1506 categorycode => $borrower->{categorycode},
1507 itemtype => $item_object->effective_itemtype,
1508 branchcode => $branchcode,
1509 rule_name => 'auto_renew'
1513 $auto_renew = $rule->rule_value if $rule;
1516 my $issue_attributes = {
1517 borrowernumber => $borrower->{'borrowernumber'},
1518 issuedate => $issuedate->strftime('%Y-%m-%d %H:%M:%S'),
1519 date_due => $datedue->strftime('%Y-%m-%d %H:%M:%S'),
1520 branchcode => C4::Context->userenv->{'branch'},
1521 onsite_checkout => $onsite_checkout,
1522 auto_renew => $auto_renew ? 1 : 0,
1525 # In the case that the borrower has an on-site checkout
1526 # and SwitchOnSiteCheckouts is enabled this converts it to a regular checkout
1527 $issue = Koha::Checkouts->find( { itemnumber => $item_object->itemnumber } );
1529 $issue->set($issue_attributes)->store;
1532 $issue = Koha::Checkout->new(
1534 itemnumber => $item_object->itemnumber,
1539 if ( $item_object->location && $item_object->location eq 'CART'
1540 && ( !$item_object->permanent_location || $item_object->permanent_location ne 'CART' ) ) {
1541 ## Item was moved to cart via UpdateItemLocationOnCheckin, anything issued should be taken off the cart.
1542 CartToShelf( $item_object->itemnumber );
1545 if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) {
1546 UpdateTotalIssues( $item_object->biblionumber, 1 );
1549 # Record if item was lost
1550 my $was_lost = $item_object->itemlost;
1552 $item_object->issues( ( $item_object->issues || 0 ) + 1);
1553 $item_object->holdingbranch(C4::Context->userenv->{'branch'});
1554 $item_object->itemlost(0);
1555 $item_object->onloan($datedue->ymd());
1556 $item_object->datelastborrowed( dt_from_string()->ymd() );
1557 $item_object->datelastseen( dt_from_string()->ymd() );
1558 $item_object->store({log_action => 0});
1560 # If the item was lost, it has now been found, charge the overdue if necessary
1562 if ( $item_object->{_charge} ) {
1563 $actualissue //= Koha::Old::Checkouts->search(
1564 { itemnumber => $item_unblessed->{itemnumber} },
1566 order_by => { '-desc' => 'returndate' },
1570 unless ( exists( $borrower->{branchcode} ) ) {
1571 my $patron = $actualissue->patron;
1572 $borrower = $patron->unblessed;
1574 _CalculateAndUpdateFine(
1576 issue => $actualissue,
1577 item => $item_unblessed,
1578 borrower => $borrower,
1579 return_date => $issuedate
1582 _FixOverduesOnReturn( $borrower->{borrowernumber},
1583 $item_object->itemnumber, undef, 'RENEWED' );
1587 # If it costs to borrow this book, charge it to the patron's account.
1588 my ( $charge, $itemtype ) = GetIssuingCharges( $item_object->itemnumber, $borrower->{'borrowernumber'} );
1589 if ( $charge && $charge > 0 ) {
1590 AddIssuingCharge( $issue, $charge, 'RENT' );
1593 my $itemtype_object = Koha::ItemTypes->find( $item_object->effective_itemtype );
1594 if ( $itemtype_object ) {
1595 my $accumulate_charge = $fees->accumulate_rentalcharge();
1596 if ( $accumulate_charge > 0 ) {
1597 AddIssuingCharge( $issue, $accumulate_charge, 'RENT_DAILY' );
1598 $charge += $accumulate_charge;
1599 $item_unblessed->{charge} = $charge;
1603 # Record the fact that this book was issued.
1606 branch => C4::Context->userenv->{'branch'},
1607 type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
1609 other => ( $sipmode ? "SIP-$sipmode" : '' ),
1610 itemnumber => $item_object->itemnumber,
1611 itemtype => $item_object->effective_itemtype,
1612 location => $item_object->location,
1613 borrowernumber => $borrower->{'borrowernumber'},
1614 ccode => $item_object->ccode,
1618 # Send a checkout slip.
1619 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1621 branchcode => $branchcode,
1622 categorycode => $borrower->{categorycode},
1623 item_type => $item_object->effective_itemtype,
1624 notification => 'CHECKOUT',
1626 if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
1627 SendCirculationAlert(
1630 item => $item_object->unblessed,
1631 borrower => $borrower,
1632 branch => $branchcode,
1637 "CIRCULATION", "ISSUE",
1638 $borrower->{'borrowernumber'},
1639 $item_object->itemnumber,
1640 ) if C4::Context->preference("IssueLog");
1642 Koha::Plugins->call('after_circ_action', {
1643 action => 'checkout',
1645 type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
1646 checkout => $issue->get_from_storage
1654 =head2 GetLoanLength
1656 my $loanlength = &GetLoanLength($borrowertype,$itemtype,branchcode)
1658 Get loan length for an itemtype, a borrower type and a branch
1663 my ( $categorycode, $itemtype, $branchcode ) = @_;
1665 # Initialize default values
1669 lengthunit => 'days',
1672 my $found = Koha::CirculationRules->get_effective_rules( {
1673 branchcode => $branchcode,
1674 categorycode => $categorycode,
1675 itemtype => $itemtype,
1684 foreach my $rule_name (keys %$found) {
1685 $rules->{$rule_name} = $found->{$rule_name};
1692 =head2 GetHardDueDate
1694 my ($hardduedate,$hardduedatecompare) = &GetHardDueDate($borrowertype,$itemtype,branchcode)
1696 Get the Hard Due Date and it's comparison for an itemtype, a borrower type and a branch
1700 sub GetHardDueDate {
1701 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1703 my $rules = Koha::CirculationRules->get_effective_rules(
1705 categorycode => $borrowertype,
1706 itemtype => $itemtype,
1707 branchcode => $branchcode,
1708 rules => [ 'hardduedate', 'hardduedatecompare' ],
1712 if ( defined( $rules->{hardduedate} ) ) {
1713 if ( $rules->{hardduedate} ) {
1714 return ( dt_from_string( $rules->{hardduedate}, 'iso' ), $rules->{hardduedatecompare} );
1717 return ( undef, undef );
1722 =head2 GetBranchBorrowerCircRule
1724 my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
1726 Retrieves circulation rule attributes that apply to the given
1727 branch and patron category, regardless of item type.
1728 The return value is a hashref containing the following key:
1730 patron_maxissueqty - maximum number of loans that a
1731 patron of the given category can have at the given
1732 branch. If the value is undef, no limit.
1734 patron_maxonsiteissueqty - maximum of on-site checkouts that a
1735 patron of the given category can have at the given
1736 branch. If the value is undef, no limit.
1738 This will check for different branch/category combinations in the following order:
1742 default branch and category
1744 If no rule has been found in the database, it will default to
1747 patron_maxissueqty - undef
1748 patron_maxonsiteissueqty - undef
1750 C<$branchcode> and C<$categorycode> should contain the
1751 literal branch code and patron category code, respectively - no
1756 sub GetBranchBorrowerCircRule {
1757 my ( $branchcode, $categorycode ) = @_;
1759 # Initialize default values
1761 patron_maxissueqty => undef,
1762 patron_maxonsiteissueqty => undef,
1766 foreach my $rule_name (qw( patron_maxissueqty patron_maxonsiteissueqty )) {
1767 my $rule = Koha::CirculationRules->get_effective_rule(
1769 categorycode => $categorycode,
1771 branchcode => $branchcode,
1772 rule_name => $rule_name,
1776 $rules->{$rule_name} = $rule->rule_value if defined $rule;
1782 =head2 GetBranchItemRule
1784 my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
1786 Retrieves circulation rule attributes that apply to the given
1787 branch and item type, regardless of patron category.
1789 The return value is a hashref containing the following keys:
1791 holdallowed => Hold policy for this branch and itemtype. Possible values:
1792 0: No holds allowed.
1793 1: Holds allowed only by patrons that have the same homebranch as the item.
1794 2: Holds allowed from any patron.
1796 returnbranch => branch to which to return item. Possible values:
1797 noreturn: do not return, let item remain where checked in (floating collections)
1798 homebranch: return to item's home branch
1799 holdingbranch: return to issuer branch
1801 This searches branchitemrules in the following order:
1803 * Same branchcode and itemtype
1804 * Same branchcode, itemtype '*'
1805 * branchcode '*', same itemtype
1806 * branchcode and itemtype '*'
1808 Neither C<$branchcode> nor C<$itemtype> should be '*'.
1812 sub GetBranchItemRule {
1813 my ( $branchcode, $itemtype ) = @_;
1816 my $holdallowed_rule = Koha::CirculationRules->get_effective_rule(
1818 branchcode => $branchcode,
1819 itemtype => $itemtype,
1820 rule_name => 'holdallowed',
1823 my $hold_fulfillment_policy_rule = Koha::CirculationRules->get_effective_rule(
1825 branchcode => $branchcode,
1826 itemtype => $itemtype,
1827 rule_name => 'hold_fulfillment_policy',
1830 my $returnbranch_rule = Koha::CirculationRules->get_effective_rule(
1832 branchcode => $branchcode,
1833 itemtype => $itemtype,
1834 rule_name => 'returnbranch',
1838 # built-in default circulation rule
1840 $rules->{holdallowed} = defined $holdallowed_rule
1841 ? $holdallowed_rule->rule_value
1843 $rules->{hold_fulfillment_policy} = defined $hold_fulfillment_policy_rule
1844 ? $hold_fulfillment_policy_rule->rule_value
1846 $rules->{returnbranch} = defined $returnbranch_rule
1847 ? $returnbranch_rule->rule_value
1855 ($doreturn, $messages, $iteminformation, $borrower) =
1856 &AddReturn( $barcode, $branch [,$exemptfine] [,$returndate] );
1862 =item C<$barcode> is the bar code of the book being returned.
1864 =item C<$branch> is the code of the branch where the book is being returned.
1866 =item C<$exemptfine> indicates that overdue charges for the item will be
1869 =item C<$return_date> allows the default return date to be overridden
1870 by the given return date. Optional.
1874 C<&AddReturn> returns a list of four items:
1876 C<$doreturn> is true iff the return succeeded.
1878 C<$messages> is a reference-to-hash giving feedback on the operation.
1879 The keys of the hash are:
1885 No item with this barcode exists. The value is C<$barcode>.
1889 The book is not currently on loan. The value is C<$barcode>.
1893 This book has been withdrawn/cancelled. The value should be ignored.
1895 =item C<Wrongbranch>
1897 This book has was returned to the wrong branch. The value is a hashref
1898 so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
1899 contain the branchcode of the incorrect and correct return library, respectively.
1903 The item was reserved. The value is a reference-to-hash whose keys are
1904 fields from the reserves table of the Koha database, and
1905 C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
1906 either C<Waiting>, C<Reserved>, or 0.
1908 =item C<WasReturned>
1910 Value 1 if return is successful.
1912 =item C<NeedsTransfer>
1914 If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer.
1918 C<$iteminformation> is a reference-to-hash, giving information about the
1919 returned item from the issues table.
1921 C<$borrower> is a reference-to-hash, giving information about the
1922 patron who last borrowed the book.
1927 my ( $barcode, $branch, $exemptfine, $return_date ) = @_;
1929 if ($branch and not Koha::Libraries->find($branch)) {
1930 warn "AddReturn error: branch '$branch' not found. Reverting to " . C4::Context->userenv->{'branch'};
1933 $branch = C4::Context->userenv->{'branch'} unless $branch; # we trust userenv to be a safe fallback/default
1934 my $return_date_specified = !!$return_date;
1935 $return_date //= dt_from_string();
1939 my $validTransfer = 1;
1940 my $stat_type = 'return';
1942 # get information on item
1943 my $item = Koha::Items->find({ barcode => $barcode });
1945 return ( 0, { BadBarcode => $barcode } ); # no barcode means no item or borrower. bail out.
1948 my $itemnumber = $item->itemnumber;
1949 my $itemtype = $item->effective_itemtype;
1951 my $issue = $item->checkout;
1953 $patron = $issue->patron
1954 or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existent borrowernumber '" . $issue->borrowernumber . "'\n"
1955 . Dumper($issue->unblessed) . "\n";
1957 $messages->{'NotIssued'} = $barcode;
1958 $item->onloan(undef)->store({skip_record_index=>1}) if defined $item->onloan;
1960 # even though item is not on loan, it may still be transferred; therefore, get current branch info
1962 # No issue, no borrowernumber. ONLY if $doreturn, *might* you have a $borrower later.
1963 # Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
1964 if (C4::Context->preference("RecordLocalUseOnReturn")) {
1965 $messages->{'LocalUse'} = 1;
1966 $stat_type = 'localuse';
1970 # full item data, but no borrowernumber or checkout info (no issue)
1971 my $hbr = GetBranchItemRule($item->homebranch, $itemtype)->{'returnbranch'} || "homebranch";
1972 # get the proper branch to which to return the item
1973 my $returnbranch = $hbr ne 'noreturn' ? $item->$hbr : $branch;
1974 # if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
1975 my $transfer_trigger = $hbr eq 'homebranch' ? 'ReturnToHome' : $hbr eq 'holdingbranch' ? 'ReturnToHolding' : undef;
1977 my $borrowernumber = $patron ? $patron->borrowernumber : undef; # we don't know if we had a borrower or not
1978 my $patron_unblessed = $patron ? $patron->unblessed : {};
1980 my $update_loc_rules = get_yaml_pref_hash('UpdateItemLocationOnCheckin');
1981 map { $update_loc_rules->{$_} = $update_loc_rules->{$_}[0] } keys %$update_loc_rules; #We can only move to one location so we flatten the arrays
1982 if ($update_loc_rules) {
1983 if (defined $update_loc_rules->{_ALL_}) {
1984 if ($update_loc_rules->{_ALL_} eq '_PERM_') { $update_loc_rules->{_ALL_} = $item->permanent_location; }
1985 if ($update_loc_rules->{_ALL_} eq '_BLANK_') { $update_loc_rules->{_ALL_} = ''; }
1986 if ( defined $item->location && $item->location ne $update_loc_rules->{_ALL_}) {
1987 $messages->{'ItemLocationUpdated'} = { from => $item->location, to => $update_loc_rules->{_ALL_} };
1988 $item->location($update_loc_rules->{_ALL_})->store({skip_record_index=>1});
1992 foreach my $key ( keys %$update_loc_rules ) {
1993 if ( $update_loc_rules->{$key} eq '_PERM_' ) { $update_loc_rules->{$key} = $item->permanent_location; }
1994 if ( $update_loc_rules->{$key} eq '_BLANK_') { $update_loc_rules->{$key} = '' ;}
1995 if ( ($item->location eq $key && $item->location ne $update_loc_rules->{$key}) || ($key eq '_BLANK_' && $item->location eq '' && $update_loc_rules->{$key} ne '') ) {
1996 $messages->{'ItemLocationUpdated'} = { from => $item->location, to => $update_loc_rules->{$key} };
1997 $item->location($update_loc_rules->{$key})->store({skip_record_index=>1});
2004 my $yaml = C4::Context->preference('UpdateNotForLoanStatusOnCheckin');
2006 $yaml = "$yaml\n\n"; # YAML is anal on ending \n. Surplus does not hurt
2008 eval { $rules = YAML::Load($yaml); };
2010 warn "Unable to parse UpdateNotForLoanStatusOnCheckin syspref : $@";
2013 foreach my $key ( keys %$rules ) {
2014 if ( $item->notforloan eq $key ) {
2015 $messages->{'NotForLoanStatusUpdated'} = { from => $item->notforloan, to => $rules->{$key} };
2016 $item->notforloan($rules->{$key})->store({ log_action => 0, skip_record_index => 1 });
2023 # check if the return is allowed at this branch
2024 my ($returnallowed, $message) = CanBookBeReturned($item->unblessed, $branch);
2025 unless ($returnallowed){
2026 $messages->{'Wrongbranch'} = {
2027 Wrongbranch => $branch,
2028 Rightbranch => $message
2031 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
2032 $indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
2033 return ( $doreturn, $messages, $issue, $patron_unblessed);
2036 if ( $item->withdrawn ) { # book has been cancelled
2037 $messages->{'withdrawn'} = 1;
2038 $doreturn = 0 if C4::Context->preference("BlockReturnOfWithdrawnItems");
2041 if ( $item->itemlost and C4::Context->preference("BlockReturnOfLostItems") ) {
2045 # case of a return of document (deal with issues and holdingbranch)
2047 die "The item is not issed and cannot be returned" unless $issue; # Just in case...
2048 $patron or warn "AddReturn without current borrower";
2052 MarkIssueReturned( $borrowernumber, $item->itemnumber, $return_date, $patron->privacy, { skip_record_index => 1} );
2057 C4::Context->preference('CalculateFinesOnReturn')
2058 || ( $return_date_specified && C4::Context->preference('CalculateFinesOnBackdate') )
2063 _CalculateAndUpdateFine( { issue => $issue, item => $item->unblessed, borrower => $patron_unblessed, return_date => $return_date } );
2066 carp "The checkin for the following issue failed, Please go to the about page, section 'data corrupted' to know how to fix this problem ($@)" . Dumper( $issue->unblessed );
2068 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
2069 $indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
2071 return ( 0, { WasReturned => 0, DataCorrupted => 1 }, $issue, $patron_unblessed );
2074 # FIXME is the "= 1" right? This could be the borrower hash.
2075 $messages->{'WasReturned'} = 1;
2078 $item->onloan(undef)->store({ log_action => 0 , skip_record_index => 1 });
2082 # the holdingbranch is updated if the document is returned to another location.
2083 # this is always done regardless of whether the item was on loan or not
2084 if ($item->holdingbranch ne $branch) {
2085 $item->holdingbranch($branch)->store({ skip_record_index => 1 });
2088 my $item_was_lost = $item->itemlost;
2089 my $leave_item_lost = C4::Context->preference("BlockReturnOfLostItems") ? 1 : 0;
2090 my $updated_item = ModDateLastSeen( $item->itemnumber, $leave_item_lost, { skip_record_index => 1 } ); # will unset itemlost if needed
2092 # fix up the accounts.....
2093 if ($item_was_lost) {
2094 $messages->{'WasLost'} = 1;
2095 unless ( C4::Context->preference("BlockReturnOfLostItems") ) {
2096 $messages->{'LostItemFeeRefunded'} = $updated_item->{_refunded};
2097 $messages->{'LostItemFeeRestored'} = $updated_item->{_restored};
2099 if ( $updated_item->{_charge} ) {
2100 $issue //= Koha::Old::Checkouts->search(
2101 { itemnumber => $item->itemnumber },
2102 { order_by => { '-desc' => 'returndate' }, rows => 1 } )
2104 unless ( exists( $patron_unblessed->{branchcode} ) ) {
2105 my $patron = $issue->patron;
2106 $patron_unblessed = $patron->unblessed;
2108 _CalculateAndUpdateFine(
2111 item => $item->unblessed,
2112 borrower => $patron_unblessed,
2113 return_date => $return_date
2116 _FixOverduesOnReturn( $patron_unblessed->{borrowernumber},
2117 $item->itemnumber, undef, 'RETURNED' );
2118 $messages->{'LostItemFeeCharged'} = 1;
2123 # check if we have a transfer for this document
2124 my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->itemnumber );
2126 # if we have a transfer to complete, we update the line of transfers with the datearrived
2127 my $is_in_rotating_collection = C4::RotatingCollections::isItemInAnyCollection( $item->itemnumber );
2129 # At this point we will either fill the transfer or it is a wrong transfer
2130 # either way we should not now generate a new transfer
2132 if ( $tobranch eq $branch ) {
2133 my $sth = C4::Context->dbh->prepare(
2134 "UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL"
2136 $sth->execute( $item->itemnumber );
2137 $messages->{'TransferArrived'} = $frombranch;
2139 $messages->{'WrongTransfer'} = $tobranch;
2140 $messages->{'WrongTransferItem'} = $item->itemnumber;
2144 # fix up the overdues in accounts...
2145 if ($borrowernumber) {
2146 my $fix = _FixOverduesOnReturn( $borrowernumber, $item->itemnumber, $exemptfine, 'RETURNED' );
2147 defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, ".$item->itemnumber."...) failed!"; # zero is OK, check defined
2149 if ( $issue and $issue->is_overdue($return_date) ) {
2151 my ($debardate,$reminder) = _debar_user_on_return( $patron_unblessed, $item->unblessed, dt_from_string($issue->date_due), $return_date );
2153 $messages->{'PrevDebarred'} = $debardate;
2155 $messages->{'Debarred'} = $debardate if $debardate;
2157 # there's no overdue on the item but borrower had been previously debarred
2158 } elsif ( $issue->date_due and $patron->debarred ) {
2159 if ( $patron->debarred eq "9999-12-31") {
2160 $messages->{'ForeverDebarred'} = $patron->debarred;
2162 my $borrower_debar_dt = dt_from_string( $patron->debarred );
2163 $borrower_debar_dt->truncate(to => 'day');
2164 my $today_dt = $return_date->clone()->truncate(to => 'day');
2165 if ( DateTime->compare( $borrower_debar_dt, $today_dt ) != -1 ) {
2166 $messages->{'PrevDebarred'} = $patron->debarred;
2172 # find reserves.....
2173 # launch the Checkreserves routine to find any holds
2174 my ($resfound, $resrec);
2175 my $lookahead= C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
2176 ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->itemnumber, undef, $lookahead ) unless ( $item->withdrawn );
2177 # 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)
2178 if ( $resfound and $resfound eq "Waiting" and $branch ne $resrec->{branchcode} ) {
2179 my $hold = C4::Reserves::RevertWaitingStatus( { itemnumber => $item->itemnumber } );
2180 $resfound = 'Reserved';
2181 $resrec = $hold->unblessed;
2184 $resrec->{'ResFound'} = $resfound;
2185 $messages->{'ResFound'} = $resrec;
2188 # Record the fact that this book was returned.
2192 itemnumber => $itemnumber,
2193 itemtype => $itemtype,
2194 location => $item->location,
2195 borrowernumber => $borrowernumber,
2196 ccode => $item->ccode,
2199 # Send a check-in slip. # NOTE: borrower may be undef. Do not try to send messages then.
2201 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2203 branchcode => $branch,
2204 categorycode => $patron->categorycode,
2205 item_type => $itemtype,
2206 notification => 'CHECKIN',
2208 if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
2209 SendCirculationAlert({
2211 item => $item->unblessed,
2212 borrower => $patron->unblessed,
2217 logaction("CIRCULATION", "RETURN", $borrowernumber, $item->itemnumber)
2218 if C4::Context->preference("ReturnLog");
2221 # Check if this item belongs to a biblio record that is attached to an
2222 # ILL request, if it is we need to update the ILL request's status
2223 if ( $doreturn and C4::Context->preference('CirculateILL')) {
2224 my $request = Koha::Illrequests->find(
2225 { biblio_id => $item->biblio->biblionumber }
2227 $request->status('RET') if $request;
2230 # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
2231 if ($validTransfer && !$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) ){
2232 my $BranchTransferLimitsType = C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ? 'effective_itemtype' : 'ccode';
2233 if (C4::Context->preference("AutomaticItemReturn" ) or
2234 (C4::Context->preference("UseBranchTransferLimits") and
2235 ! IsBranchTransferAllowed($branch, $returnbranch, $item->$BranchTransferLimitsType )
2237 $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s, %s)", $item->itemnumber,$branch, $returnbranch, $transfer_trigger;
2238 $debug and warn "item: " . Dumper($item->unblessed);
2239 ModItemTransfer($item->itemnumber, $branch, $returnbranch, $transfer_trigger, { skip_record_index => 1 });
2240 $messages->{'WasTransfered'} = 1;
2242 $messages->{'NeedsTransfer'} = $returnbranch;
2243 $messages->{'TransferTrigger'} = $transfer_trigger;
2247 if ( C4::Context->preference('ClaimReturnedLostValue') ) {
2248 my $claims = Koha::Checkouts::ReturnClaims->search(
2250 itemnumber => $item->id,
2251 resolution => undef,
2255 if ( $claims->count ) {
2256 $messages->{ReturnClaims} = $claims;
2260 my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
2261 $indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
2263 if ( $doreturn and $issue ) {
2264 my $checkin = Koha::Old::Checkouts->find($issue->id);
2266 Koha::Plugins->call('after_circ_action', {
2267 action => 'checkin',
2274 return ( $doreturn, $messages, $issue, ( $patron ? $patron->unblessed : {} ));
2277 =head2 MarkIssueReturned
2279 MarkIssueReturned($borrowernumber, $itemnumber, $returndate, $privacy, [$params] );
2281 Unconditionally marks an issue as being returned by
2282 moving the C<issues> row to C<old_issues> and
2283 setting C<returndate> to the current date.
2285 if C<$returndate> is specified (in iso format), it is used as the date
2288 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
2289 the old_issue is immediately anonymised
2291 Ideally, this function would be internal to C<C4::Circulation>,
2292 not exported, but it is currently used in misc/cronjobs/longoverdue.pl
2293 and offline_circ/process_koc.pl.
2295 The last optional parameter allos passing skip_record_index to the item store call.
2299 sub MarkIssueReturned {
2300 my ( $borrowernumber, $itemnumber, $returndate, $privacy, $params ) = @_;
2302 # Retrieve the issue
2303 my $issue = Koha::Checkouts->find( { itemnumber => $itemnumber } ) or return;
2305 return unless $issue->borrowernumber == $borrowernumber; # If the item is checked out to another patron we do not return it
2307 my $issue_id = $issue->issue_id;
2309 my $anonymouspatron;
2310 if ( $privacy && $privacy == 2 ) {
2311 # The default of 0 will not work due to foreign key constraints
2312 # The anonymisation will fail if AnonymousPatron is not a valid entry
2313 # We need to check if the anonymous patron exist, Koha will fail loudly if it does not
2314 # Note that a warning should appear on the about page (System information tab).
2315 $anonymouspatron = C4::Context->preference('AnonymousPatron');
2316 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."
2317 unless Koha::Patrons->find( $anonymouspatron );
2320 my $schema = Koha::Database->schema;
2322 # FIXME Improve the return value and handle it from callers
2323 $schema->txn_do(sub {
2325 my $patron = Koha::Patrons->find( $borrowernumber );
2327 # Update the returndate value
2328 if ( $returndate ) {
2329 $issue->returndate( $returndate )->store->discard_changes; # update and refetch
2332 $issue->returndate( \'NOW()' )->store->discard_changes; # update and refetch
2335 # Create the old_issues entry
2336 my $old_checkout = Koha::Old::Checkout->new($issue->unblessed)->store;
2338 # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
2339 if ( $privacy && $privacy == 2) {
2340 $old_checkout->borrowernumber($anonymouspatron)->store;
2343 # And finally delete the issue
2346 $issue->item->onloan(undef)->store({ log_action => 0, skip_record_index => $params->{skip_record_index} });
2348 if ( C4::Context->preference('StoreLastBorrower') ) {
2349 my $item = Koha::Items->find( $itemnumber );
2350 $item->last_returned_by( $patron );
2353 # Remove any OVERDUES related debarment if the borrower has no overdues
2354 if ( C4::Context->preference('AutoRemoveOverduesRestrictions')
2355 && $patron->debarred
2356 && !$patron->has_overdues
2357 && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2359 DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2367 =head2 _debar_user_on_return
2369 _debar_user_on_return($borrower, $item, $datedue, $returndate);
2371 C<$borrower> borrower hashref
2373 C<$item> item hashref
2375 C<$datedue> date due DateTime object
2377 C<$returndate> DateTime object representing the return time
2379 Internal function, called only by AddReturn that calculates and updates
2380 the user fine days, and debars them if necessary.
2382 Should only be called for overdue returns
2384 Calculation of the debarment date has been moved to a separate subroutine _calculate_new_debar_dt
2389 sub _calculate_new_debar_dt {
2390 my ( $borrower, $item, $dt_due, $return_date ) = @_;
2392 my $branchcode = _GetCircControlBranch( $item, $borrower );
2393 my $circcontrol = C4::Context->preference('CircControl');
2394 my $issuing_rule = Koha::CirculationRules->get_effective_rules(
2395 { categorycode => $borrower->{categorycode},
2396 itemtype => $item->{itype},
2397 branchcode => $branchcode,
2402 'maxsuspensiondays',
2403 'suspension_chargeperiod',
2407 my $finedays = $issuing_rule ? $issuing_rule->{finedays} : undef;
2408 my $unit = $issuing_rule ? $issuing_rule->{lengthunit} : undef;
2409 my $chargeable_units = C4::Overdues::get_chargeable_units($unit, $dt_due, $return_date, $branchcode);
2411 return unless $finedays;
2413 # finedays is in days, so hourly loans must multiply by 24
2414 # thus 1 hour late equals 1 day suspension * finedays rate
2415 $finedays = $finedays * 24 if ( $unit eq 'hours' );
2417 # grace period is measured in the same units as the loan
2419 DateTime::Duration->new( $unit => $issuing_rule->{firstremind} // 0);
2421 my $deltadays = DateTime::Duration->new(
2422 days => $chargeable_units
2425 if ( $deltadays->subtract($grace)->is_positive() ) {
2426 my $suspension_days = $deltadays * $finedays;
2428 if ( defined $issuing_rule->{suspension_chargeperiod} && $issuing_rule->{suspension_chargeperiod} > 1 ) {
2429 # No need to / 1 and do not consider / 0
2430 $suspension_days = DateTime::Duration->new(
2431 days => floor( $suspension_days->in_units('days') / $issuing_rule->{suspension_chargeperiod} )
2435 # If the max suspension days is < than the suspension days
2436 # the suspension days is limited to this maximum period.
2437 my $max_sd = $issuing_rule->{maxsuspensiondays};
2438 if ( defined $max_sd && $max_sd ne '' ) {
2439 $max_sd = DateTime::Duration->new( days => $max_sd );
2440 $suspension_days = $max_sd
2441 if DateTime::Duration->compare( $max_sd, $suspension_days ) < 0;
2444 my ( $has_been_extended );
2445 if ( C4::Context->preference('CumulativeRestrictionPeriods') and $borrower->{debarred} ) {
2446 my $debarment = @{ GetDebarments( { borrowernumber => $borrower->{borrowernumber}, type => 'SUSPENSION' } ) }[0];
2448 $return_date = dt_from_string( $debarment->{expiration}, 'sql' );
2449 $has_been_extended = 1;
2454 # Use the calendar or not to calculate the debarment date
2455 if ( C4::Context->preference('SuspensionsCalendar') eq 'noSuspensionsWhenClosed' ) {
2456 my $calendar = Koha::Calendar->new(
2457 branchcode => $branchcode,
2458 days_mode => 'Calendar'
2460 $new_debar_dt = $calendar->addDate( $return_date, $suspension_days );
2463 $new_debar_dt = $return_date->clone()->add_duration($suspension_days);
2465 return $new_debar_dt;
2470 sub _debar_user_on_return {
2471 my ( $borrower, $item, $dt_due, $return_date ) = @_;
2473 $return_date //= dt_from_string();
2475 my $new_debar_dt = _calculate_new_debar_dt ($borrower, $item, $dt_due, $return_date);
2477 return unless $new_debar_dt;
2479 Koha::Patron::Debarments::AddUniqueDebarment({
2480 borrowernumber => $borrower->{borrowernumber},
2481 expiration => $new_debar_dt->ymd(),
2482 type => 'SUSPENSION',
2484 # if borrower was already debarred but does not get an extra debarment
2485 my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
2486 my ($new_debarment_str, $is_a_reminder);
2487 if ( $borrower->{debarred} && $borrower->{debarred} eq $patron->is_debarred ) {
2489 $new_debarment_str = $borrower->{debarred};
2491 $new_debarment_str = $new_debar_dt->ymd();
2493 # FIXME Should return a DateTime object
2494 return $new_debarment_str, $is_a_reminder;
2497 =head2 _FixOverduesOnReturn
2499 &_FixOverduesOnReturn($borrowernumber, $itemnumber, $exemptfine, $status);
2501 C<$borrowernumber> borrowernumber
2503 C<$itemnumber> itemnumber
2505 C<$exemptfine> BOOL -- remove overdue charge associated with this issue.
2507 C<$status> ENUM -- reason for fix [ RETURNED, RENEWED, LOST, FORGIVEN ]
2513 sub _FixOverduesOnReturn {
2514 my ( $borrowernumber, $item, $exemptfine, $status ) = @_;
2515 unless( $borrowernumber ) {
2516 warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
2520 warn "_FixOverduesOnReturn() not supplied valid itemnumber";
2524 warn "_FixOverduesOnReturn() not supplied valid status";
2528 my $schema = Koha::Database->schema;
2530 my $result = $schema->txn_do(
2532 # check for overdue fine
2533 my $accountlines = Koha::Account::Lines->search(
2535 borrowernumber => $borrowernumber,
2536 itemnumber => $item,
2537 debit_type_code => 'OVERDUE',
2538 status => 'UNRETURNED'
2541 return 0 unless $accountlines->count; # no warning, there's just nothing to fix
2543 my $accountline = $accountlines->next;
2544 my $payments = $accountline->credits;
2546 my $amountoutstanding = $accountline->amountoutstanding;
2547 if ( $accountline->amount == 0 && $payments->count == 0 ) {
2548 $accountline->delete;
2549 return 0; # no warning, we've just removed a zero value fine (backdated return)
2550 } elsif ($exemptfine && ($amountoutstanding != 0)) {
2551 my $account = Koha::Account->new({patron_id => $borrowernumber});
2552 my $credit = $account->add_credit(
2554 amount => $amountoutstanding,
2555 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
2556 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
2557 interface => C4::Context->interface,
2563 $credit->apply({ debits => [ $accountline ], offset_type => 'Forgiven' });
2565 if (C4::Context->preference("FinesLog")) {
2566 &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
2570 $accountline->status($status);
2571 return $accountline->store();
2578 =head2 _GetCircControlBranch
2580 my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
2584 Return the library code to be used to determine which circulation
2585 policy applies to a transaction. Looks up the CircControl and
2586 HomeOrHoldingBranch system preferences.
2588 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
2590 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
2594 sub _GetCircControlBranch {
2595 my ($item, $borrower) = @_;
2596 my $circcontrol = C4::Context->preference('CircControl');
2599 if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
2600 $branch= C4::Context->userenv->{'branch'};
2601 } elsif ($circcontrol eq 'PatronLibrary') {
2602 $branch=$borrower->{branchcode};
2604 my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
2605 $branch = $item->{$branchfield};
2606 # default to item home branch if holdingbranch is used
2607 # and is not defined
2608 if (!defined($branch) && $branchfield eq 'holdingbranch') {
2609 $branch = $item->{homebranch};
2617 $issue = GetOpenIssue( $itemnumber );
2619 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2621 C<$itemnumber> is the item's itemnumber
2628 my ( $itemnumber ) = @_;
2629 return unless $itemnumber;
2630 my $dbh = C4::Context->dbh;
2631 my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2632 $sth->execute( $itemnumber );
2633 return $sth->fetchrow_hashref();
2637 =head2 GetBiblioIssues
2639 $issues = GetBiblioIssues($biblionumber);
2641 this function get all issues from a biblionumber.
2644 C<$issues> is a reference to array which each value is ref-to-hash. This ref-to-hash contains all column from
2645 tables issues and the firstname,surname & cardnumber from borrowers.
2649 sub GetBiblioIssues {
2650 my $biblionumber = shift;
2651 return unless $biblionumber;
2652 my $dbh = C4::Context->dbh;
2654 SELECT issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2656 LEFT JOIN borrowers ON borrowers.borrowernumber = issues.borrowernumber
2657 LEFT JOIN items ON issues.itemnumber = items.itemnumber
2658 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2659 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2660 WHERE biblio.biblionumber = ?
2662 SELECT old_issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2664 LEFT JOIN borrowers ON borrowers.borrowernumber = old_issues.borrowernumber
2665 LEFT JOIN items ON old_issues.itemnumber = items.itemnumber
2666 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2667 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2668 WHERE biblio.biblionumber = ?
2671 my $sth = $dbh->prepare($query);
2672 $sth->execute($biblionumber, $biblionumber);
2675 while ( my $data = $sth->fetchrow_hashref ) {
2676 push @issues, $data;
2681 =head2 GetUpcomingDueIssues
2683 my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2687 sub GetUpcomingDueIssues {
2690 $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2691 my $dbh = C4::Context->dbh;
2693 my $statement = <<END_SQL;
2696 SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2698 LEFT JOIN items USING (itemnumber)
2699 LEFT OUTER JOIN branches USING (branchcode)
2700 WHERE returndate is NULL
2702 WHERE days_until_due >= 0 AND days_until_due <= ?
2705 my @bind_parameters = ( $params->{'days_in_advance'} );
2707 my $sth = $dbh->prepare( $statement );
2708 $sth->execute( @bind_parameters );
2709 my $upcoming_dues = $sth->fetchall_arrayref({});
2711 return $upcoming_dues;
2714 =head2 CanBookBeRenewed
2716 ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber[, $override_limit]);
2718 Find out whether a borrowed item may be renewed.
2720 C<$borrowernumber> is the borrower number of the patron who currently
2721 has the item on loan.
2723 C<$itemnumber> is the number of the item to renew.
2725 C<$override_limit>, if supplied with a true value, causes
2726 the limit on the number of times that the loan can be renewed
2727 (as controlled by the item type) to be ignored. Overriding also allows
2728 to renew sooner than "No renewal before" and to manually renew loans
2729 that are automatically renewed.
2731 C<$CanBookBeRenewed> returns a true value if the item may be renewed. The
2732 item must currently be on loan to the specified borrower; renewals
2733 must be allowed for the item's type; and the borrower must not have
2734 already renewed the loan. $error will contain the reason the renewal can not proceed
2738 sub CanBookBeRenewed {
2739 my ( $borrowernumber, $itemnumber, $override_limit, $cron ) = @_;
2741 my $dbh = C4::Context->dbh;
2743 my $auto_renew = "no";
2745 my $item = Koha::Items->find($itemnumber) or return ( 0, 'no_item' );
2746 my $issue = $item->checkout or return ( 0, 'no_checkout' );
2747 return ( 0, 'onsite_checkout' ) if $issue->onsite_checkout;
2748 return ( 0, 'item_denied_renewal') if _item_denied_renewal({ item => $item });
2750 my $patron = $issue->patron or return;
2752 # override_limit will override anything else except on_reserve
2753 unless ( $override_limit ){
2754 my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
2755 my $issuing_rule = Koha::CirculationRules->get_effective_rules(
2757 categorycode => $patron->categorycode,
2758 itemtype => $item->effective_itemtype,
2759 branchcode => $branchcode,
2762 'no_auto_renewal_after',
2763 'no_auto_renewal_after_hard_limit',
2770 return ( 0, "too_many" )
2771 if not $issuing_rule->{renewalsallowed} or $issuing_rule->{renewalsallowed} <= $issue->renewals;
2773 my $overduesblockrenewing = C4::Context->preference('OverduesBlockRenewing');
2774 my $restrictionblockrenewing = C4::Context->preference('RestrictionBlockRenewing');
2775 $patron = Koha::Patrons->find($borrowernumber); # FIXME Is this really useful?
2776 my $restricted = $patron->is_debarred;
2777 my $hasoverdues = $patron->has_overdues;
2779 if ( $restricted and $restrictionblockrenewing ) {
2780 return ( 0, 'restriction');
2781 } elsif ( ($hasoverdues and $overduesblockrenewing eq 'block') || ($issue->is_overdue and $overduesblockrenewing eq 'blockitem') ) {
2782 return ( 0, 'overdue');
2785 if ( $issue->auto_renew && $patron->autorenew_checkouts ) {
2787 if ( $patron->category->effective_BlockExpiredPatronOpacActions and $patron->is_expired ) {
2788 return ( 0, 'auto_account_expired' );
2791 if ( defined $issuing_rule->{no_auto_renewal_after}
2792 and $issuing_rule->{no_auto_renewal_after} ne "" ) {
2793 # Get issue_date and add no_auto_renewal_after
2794 # If this is greater than today, it's too late for renewal.
2795 my $maximum_renewal_date = dt_from_string($issue->issuedate, 'sql');
2796 $maximum_renewal_date->add(
2797 $issuing_rule->{lengthunit} => $issuing_rule->{no_auto_renewal_after}
2799 my $now = dt_from_string;
2800 if ( $now >= $maximum_renewal_date ) {
2801 return ( 0, "auto_too_late" );
2804 if ( defined $issuing_rule->{no_auto_renewal_after_hard_limit}
2805 and $issuing_rule->{no_auto_renewal_after_hard_limit} ne "" ) {
2806 # If no_auto_renewal_after_hard_limit is >= today, it's also too late for renewal
2807 if ( dt_from_string >= dt_from_string( $issuing_rule->{no_auto_renewal_after_hard_limit} ) ) {
2808 return ( 0, "auto_too_late" );
2812 if ( C4::Context->preference('OPACFineNoRenewalsBlockAutoRenew') ) {
2813 my $fine_no_renewals = C4::Context->preference("OPACFineNoRenewals");
2814 my $amountoutstanding =
2815 C4::Context->preference("OPACFineNoRenewalsIncludeCredit")
2816 ? $patron->account->balance
2817 : $patron->account->outstanding_debits->total_outstanding;
2818 if ( $amountoutstanding and $amountoutstanding > $fine_no_renewals ) {
2819 return ( 0, "auto_too_much_oweing" );
2824 if ( defined $issuing_rule->{norenewalbefore}
2825 and $issuing_rule->{norenewalbefore} ne "" )
2828 # Calculate soonest renewal by subtracting 'No renewal before' from due date
2829 my $soonestrenewal = dt_from_string( $issue->date_due, 'sql' )->subtract(
2830 $issuing_rule->{lengthunit} => $issuing_rule->{norenewalbefore} );
2832 # Depending on syspref reset the exact time, only check the date
2833 if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
2834 and $issuing_rule->{lengthunit} eq 'days' )
2836 $soonestrenewal->truncate( to => 'day' );
2839 if ( $soonestrenewal > dt_from_string() )
2841 $auto_renew = ($issue->auto_renew && $patron->autorenew_checkouts) ? "auto_too_soon" : "too_soon";
2843 elsif ( $issue->auto_renew && $patron->autorenew_checkouts ) {
2848 # Fallback for automatic renewals:
2849 # If norenewalbefore is undef, don't renew before due date.
2850 if ( $issue->auto_renew && $auto_renew eq "no" && $patron->autorenew_checkouts ) {
2851 my $now = dt_from_string;
2852 if ( $now >= dt_from_string( $issue->date_due, 'sql' ) ){
2855 $auto_renew = "auto_too_soon";
2860 my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2862 # If next hold is non priority, then check if any hold with priority (non_priority = 0) exists for the same biblionumber.
2863 if ( $resfound && $resrec->{non_priority} ) {
2864 $resfound = Koha::Holds->search(
2865 { biblionumber => $resrec->{biblionumber}, non_priority => 0 } )
2871 # This item can fill one or more unfilled reserve, can those unfilled reserves
2872 # all be filled by other available items?
2874 && C4::Context->preference('AllowRenewalIfOtherItemsAvailable') )
2876 my $schema = Koha::Database->new()->schema();
2878 my $item_holds = $schema->resultset('Reserve')->search( { itemnumber => $itemnumber, found => undef } )->count();
2880 # There is an item level hold on this item, no other item can fill the hold
2885 # Get all other items that could possibly fill reserves
2886 my @itemnumbers = $schema->resultset('Item')->search(
2888 biblionumber => $resrec->{biblionumber},
2891 -not => { itemnumber => $itemnumber }
2893 { columns => 'itemnumber' }
2894 )->get_column('itemnumber')->all();
2896 # Get all other reserves that could have been filled by this item
2897 my @borrowernumbers;
2899 my ( $reserve_found, $reserve, undef ) =
2900 C4::Reserves::CheckReserves( $itemnumber, undef, undef, \@borrowernumbers );
2902 if ($reserve_found) {
2903 push( @borrowernumbers, $reserve->{borrowernumber} );
2910 # If the count of the union of the lists of reservable items for each borrower
2911 # is equal or greater than the number of borrowers, we know that all reserves
2912 # can be filled with available items. We can get the union of the sets simply
2913 # by pushing all the elements onto an array and removing the duplicates.
2916 ITEM: foreach my $itemnumber (@itemnumbers) {
2917 my $item = Koha::Items->find( $itemnumber );
2918 next if IsItemOnHoldAndFound( $itemnumber );
2919 for my $borrowernumber (@borrowernumbers) {
2920 my $patron = $patrons{$borrowernumber} //= Koha::Patrons->find( $borrowernumber );
2921 next unless IsAvailableForItemLevelRequest($item, $patron);
2922 next unless CanItemBeReserved($borrowernumber,$itemnumber);
2924 push @reservable, $itemnumber;
2925 if (@reservable >= @borrowernumbers) {
2934 if( $cron ) { #The cron wants to return 'too_soon' over 'on_reserve'
2935 return ( 0, $auto_renew ) if $auto_renew =~ 'too_soon';#$auto_renew ne "no" && $auto_renew ne "ok";
2936 return ( 0, "on_reserve" ) if $resfound; # '' when no hold was found
2937 } else { # For other purposes we want 'on_reserve' before 'too_soon'
2938 return ( 0, "on_reserve" ) if $resfound; # '' when no hold was found
2939 return ( 0, $auto_renew ) if $auto_renew =~ 'too_soon';#$auto_renew ne "no" && $auto_renew ne "ok";
2942 return ( 0, "auto_renew" ) if $auto_renew eq "ok" && !$override_limit; # 0 if auto-renewal should not succeed
2944 return ( 1, undef );
2949 &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
2953 C<$borrowernumber> is the borrower number of the patron who currently
2956 C<$itemnumber> is the number of the item to renew.
2958 C<$branch> is the library where the renewal took place (if any).
2959 The library that controls the circ policies for the renewal is retrieved from the issues record.
2961 C<$datedue> can be a DateTime object used to set the due date.
2963 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate. If
2964 this parameter is not supplied, lastreneweddate is set to the current date.
2966 C<$skipfinecalc> is an optional boolean. There may be circumstances where, even if the
2967 CalculateFinesOnReturn syspref is enabled, we don't want to calculate fines upon renew,
2968 for example, when we're renewing as a result of a fine being paid (see RenewAccruingItemWhenPaid
2971 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2972 from the book's item type.
2977 my $borrowernumber = shift;
2978 my $itemnumber = shift or return;
2980 my $datedue = shift;
2981 my $lastreneweddate = shift || dt_from_string();
2982 my $skipfinecalc = shift;
2984 my $item_object = Koha::Items->find($itemnumber) or return;
2985 my $biblio = $item_object->biblio;
2986 my $issue = $item_object->checkout;
2987 my $item_unblessed = $item_object->unblessed;
2989 my $dbh = C4::Context->dbh;
2991 return unless $issue;
2993 $borrowernumber ||= $issue->borrowernumber;
2995 if ( defined $datedue && ref $datedue ne 'DateTime' ) {
2996 carp 'Invalid date passed to AddRenewal.';
3000 my $patron = Koha::Patrons->find( $borrowernumber ) or return; # FIXME Should do more than just return
3001 my $patron_unblessed = $patron->unblessed;
3003 my $circ_library = Koha::Libraries->find( _GetCircControlBranch($item_unblessed, $patron_unblessed) );
3005 my $schema = Koha::Database->schema;
3006 $schema->txn_do(sub{
3008 if ( !$skipfinecalc && C4::Context->preference('CalculateFinesOnReturn') ) {
3009 _CalculateAndUpdateFine( { issue => $issue, item => $item_unblessed, borrower => $patron_unblessed } );
3011 _FixOverduesOnReturn( $borrowernumber, $itemnumber, undef, 'RENEWED' );
3013 # If the due date wasn't specified, calculate it by adding the
3014 # book's loan length to today's date or the current due date
3015 # based on the value of the RenewalPeriodBase syspref.
3016 my $itemtype = $item_object->effective_itemtype;
3019 $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
3020 dt_from_string( $issue->date_due, 'sql' ) :
3022 $datedue = CalcDateDue($datedue, $itemtype, $circ_library->branchcode, $patron_unblessed, 'is a renewal');
3025 my $fees = Koha::Charges::Fees->new(
3028 library => $circ_library,
3029 item => $item_object,
3030 from_date => dt_from_string( $issue->date_due, 'sql' ),
3031 to_date => dt_from_string($datedue),
3035 # Update the issues record to have the new due date, and a new count
3036 # of how many times it has been renewed.
3037 my $renews = ( $issue->renewals || 0 ) + 1;
3038 my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
3039 WHERE borrowernumber=?
3043 $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
3045 # Update the renewal count on the item, and tell zebra to reindex
3046 $renews = ( $item_object->renewals || 0 ) + 1;
3047 $item_object->renewals($renews);
3048 $item_object->onloan($datedue);
3049 $item_object->store({ log_action => 0 });
3051 # Charge a new rental fee, if applicable
3052 my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
3053 if ( $charge > 0 ) {
3054 AddIssuingCharge($issue, $charge, 'RENT_RENEW');
3057 # Charge a new accumulate rental fee, if applicable
3058 my $itemtype_object = Koha::ItemTypes->find( $itemtype );
3059 if ( $itemtype_object ) {
3060 my $accumulate_charge = $fees->accumulate_rentalcharge();
3061 if ( $accumulate_charge > 0 ) {
3062 AddIssuingCharge( $issue, $accumulate_charge, 'RENT_DAILY_RENEW' )
3064 $charge += $accumulate_charge;
3067 # Send a renewal slip according to checkout alert preferencei
3068 if ( C4::Context->preference('RenewalSendNotice') eq '1' ) {
3069 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
3071 branchcode => $branch,
3072 categorycode => $patron->categorycode,
3073 item_type => $itemtype,
3074 notification => 'CHECKOUT',
3076 if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
3077 SendCirculationAlert(
3080 item => $item_unblessed,
3081 borrower => $patron->unblessed,
3088 # Remove any OVERDUES related debarment if the borrower has no overdues
3090 && $patron->is_debarred
3091 && ! $patron->has_overdues
3092 && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
3094 DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
3097 # Add the renewal to stats
3100 branch => $item_object->renewal_branchcode({branch => $branch}),
3103 itemnumber => $itemnumber,
3104 itemtype => $itemtype,
3105 location => $item_object->location,
3106 borrowernumber => $borrowernumber,
3107 ccode => $item_object->ccode,
3112 logaction("CIRCULATION", "RENEWAL", $borrowernumber, $itemnumber) if C4::Context->preference("RenewalLog");
3114 Koha::Plugins->call('after_circ_action', {
3115 action => 'renewal',
3117 checkout => $issue->get_from_storage
3126 # check renewal status
3127 my ( $bornum, $itemno ) = @_;
3128 my $dbh = C4::Context->dbh;
3130 my $renewsallowed = 0;
3133 my $patron = Koha::Patrons->find( $bornum );
3134 my $item = Koha::Items->find($itemno);
3136 return (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 # $item and $borrower should be calculated
3151 my $branchcode = _GetCircControlBranch($item->unblessed, $patron->unblessed);
3153 my $rule = Koha::CirculationRules->get_effective_rule(
3155 categorycode => $patron->categorycode,
3156 itemtype => $item->effective_itemtype,
3157 branchcode => $branchcode,
3158 rule_name => 'renewalsallowed',
3162 $renewsallowed = $rule ? $rule->rule_value : 0;
3163 $renewsleft = $renewsallowed - $renewcount;
3164 if($renewsleft < 0){ $renewsleft = 0; }
3165 return ( $renewcount, $renewsallowed, $renewsleft );
3168 =head2 GetSoonestRenewDate
3170 $NoRenewalBeforeThisDate = &GetSoonestRenewDate($borrowernumber, $itemnumber);
3172 Find out the soonest possible renew date of a borrowed item.
3174 C<$borrowernumber> is the borrower number of the patron who currently
3175 has the item on loan.
3177 C<$itemnumber> is the number of the item to renew.
3179 C<$GetSoonestRenewDate> returns the DateTime of the soonest possible
3180 renew date, based on the value "No renewal before" of the applicable
3181 issuing rule. Returns the current date if the item can already be
3182 renewed, and returns undefined if the borrower, loan, or item
3187 sub GetSoonestRenewDate {
3188 my ( $borrowernumber, $itemnumber ) = @_;
3190 my $dbh = C4::Context->dbh;
3192 my $item = Koha::Items->find($itemnumber) or return;
3193 my $itemissue = $item->checkout or return;
3195 $borrowernumber ||= $itemissue->borrowernumber;
3196 my $patron = Koha::Patrons->find( $borrowernumber )
3199 my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
3200 my $issuing_rule = Koha::CirculationRules->get_effective_rules(
3201 { categorycode => $patron->categorycode,
3202 itemtype => $item->effective_itemtype,
3203 branchcode => $branchcode,
3211 my $now = dt_from_string;
3212 return $now unless $issuing_rule;
3214 if ( defined $issuing_rule->{norenewalbefore}
3215 and $issuing_rule->{norenewalbefore} ne "" )
3217 my $soonestrenewal =
3218 dt_from_string( $itemissue->date_due )->subtract(
3219 $issuing_rule->{lengthunit} => $issuing_rule->{norenewalbefore} );
3221 if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
3222 and $issuing_rule->{lengthunit} eq 'days' )
3224 $soonestrenewal->truncate( to => 'day' );
3226 return $soonestrenewal if $now < $soonestrenewal;
3231 =head2 GetLatestAutoRenewDate
3233 $NoAutoRenewalAfterThisDate = &GetLatestAutoRenewDate($borrowernumber, $itemnumber);
3235 Find out the latest possible auto renew date of a borrowed item.
3237 C<$borrowernumber> is the borrower number of the patron who currently
3238 has the item on loan.
3240 C<$itemnumber> is the number of the item to renew.
3242 C<$GetLatestAutoRenewDate> returns the DateTime of the latest possible
3243 auto renew date, based on the value "No auto renewal after" and the "No auto
3244 renewal after (hard limit) of the applicable issuing rule.
3245 Returns undef if there is no date specify in the circ rules or if the patron, loan,
3246 or item cannot be found.
3250 sub GetLatestAutoRenewDate {
3251 my ( $borrowernumber, $itemnumber ) = @_;
3253 my $dbh = C4::Context->dbh;
3255 my $item = Koha::Items->find($itemnumber) or return;
3256 my $itemissue = $item->checkout or return;
3258 $borrowernumber ||= $itemissue->borrowernumber;
3259 my $patron = Koha::Patrons->find( $borrowernumber )
3262 my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed );
3263 my $circulation_rules = Koha::CirculationRules->get_effective_rules(
3265 categorycode => $patron->categorycode,
3266 itemtype => $item->effective_itemtype,
3267 branchcode => $branchcode,
3269 'no_auto_renewal_after',
3270 'no_auto_renewal_after_hard_limit',
3276 return unless $circulation_rules;
3278 if ( not $circulation_rules->{no_auto_renewal_after}
3279 or $circulation_rules->{no_auto_renewal_after} eq '' )
3280 and ( not $circulation_rules->{no_auto_renewal_after_hard_limit}
3281 or $circulation_rules->{no_auto_renewal_after_hard_limit} eq '' );
3283 my $maximum_renewal_date;
3284 if ( $circulation_rules->{no_auto_renewal_after} ) {
3285 $maximum_renewal_date = dt_from_string($itemissue->issuedate);
3286 $maximum_renewal_date->add(
3287 $circulation_rules->{lengthunit} => $circulation_rules->{no_auto_renewal_after}
3291 if ( $circulation_rules->{no_auto_renewal_after_hard_limit} ) {
3292 my $dt = dt_from_string( $circulation_rules->{no_auto_renewal_after_hard_limit} );
3293 $maximum_renewal_date = $dt if not $maximum_renewal_date or $maximum_renewal_date > $dt;
3295 return $maximum_renewal_date;
3299 =head2 GetIssuingCharges
3301 ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
3303 Calculate how much it would cost for a given patron to borrow a given
3304 item, including any applicable discounts.
3306 C<$itemnumber> is the item number of item the patron wishes to borrow.
3308 C<$borrowernumber> is the patron's borrower number.
3310 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
3311 and C<$item_type> is the code for the item's item type (e.g., C<VID>
3316 sub GetIssuingCharges {
3318 # calculate charges due
3319 my ( $itemnumber, $borrowernumber ) = @_;
3321 my $dbh = C4::Context->dbh;
3324 # Get the book's item type and rental charge (via its biblioitem).
3325 my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
3326 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
3327 $charge_query .= (C4::Context->preference('item-level_itypes'))
3328 ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
3329 : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
3331 $charge_query .= ' WHERE items.itemnumber =?';
3333 my $sth = $dbh->prepare($charge_query);
3334 $sth->execute($itemnumber);
3335 if ( my $item_data = $sth->fetchrow_hashref ) {
3336 $item_type = $item_data->{itemtype};
3337 $charge = $item_data->{rentalcharge};
3338 my $branch = C4::Context::mybranch();
3339 my $patron = Koha::Patrons->find( $borrowernumber );
3340 my $discount = _get_discount_from_rule($patron->categorycode, $branch, $item_type);
3342 # We may have multiple rules so get the most specific
3343 $charge = ( $charge * ( 100 - $discount ) ) / 100;
3346 $charge = sprintf '%.2f', $charge; # ensure no fractions of a penny returned
3350 return ( $charge, $item_type );
3353 # Select most appropriate discount rule from those returned
3354 sub _get_discount_from_rule {
3355 my ($categorycode, $branchcode, $itemtype) = @_;
3357 # Set search precedences
3360 branchcode => $branchcode,
3361 itemtype => $itemtype,
3362 categorycode => $categorycode,
3365 branchcode => undef,
3366 categorycode => $categorycode,
3367 itemtype => $itemtype,
3370 branchcode => $branchcode,
3371 categorycode => $categorycode,
3375 branchcode => undef,
3376 categorycode => $categorycode,
3381 foreach my $params (@params) {
3382 my $rule = Koha::CirculationRules->search(
3384 rule_name => 'rentaldiscount',
3389 return $rule->rule_value if $rule;
3396 =head2 AddIssuingCharge
3398 &AddIssuingCharge( $checkout, $charge, $type )
3402 sub AddIssuingCharge {
3403 my ( $checkout, $charge, $type ) = @_;
3405 # FIXME What if checkout does not exist?
3407 my $account = Koha::Account->new({ patron_id => $checkout->borrowernumber });
3408 my $accountline = $account->add_debit(
3412 user_id => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
3413 library_id => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
3414 interface => C4::Context->interface,
3416 item_id => $checkout->itemnumber,
3417 issue_id => $checkout->issue_id,
3424 GetTransfers($itemnumber);
3429 my ($itemnumber) = @_;
3431 my $dbh = C4::Context->dbh;
3438 FROM branchtransfers
3439 WHERE itemnumber = ?
3440 AND datearrived IS NULL
3442 my $sth = $dbh->prepare($query);
3443 $sth->execute($itemnumber);
3444 my @row = $sth->fetchrow_array();
3448 =head2 GetTransfersFromTo
3450 @results = GetTransfersFromTo($frombranch,$tobranch);
3452 Returns the list of pending transfers between $from and $to branch
3456 sub GetTransfersFromTo {
3457 my ( $frombranch, $tobranch ) = @_;
3458 return unless ( $frombranch && $tobranch );
3459 my $dbh = C4::Context->dbh;
3461 SELECT branchtransfer_id,itemnumber,datesent,frombranch
3462 FROM branchtransfers
3465 AND datearrived IS NULL
3467 my $sth = $dbh->prepare($query);
3468 $sth->execute( $frombranch, $tobranch );
3471 while ( my $data = $sth->fetchrow_hashref ) {
3472 push @gettransfers, $data;
3474 return (@gettransfers);
3477 =head2 DeleteTransfer
3479 &DeleteTransfer($itemnumber);
3483 sub DeleteTransfer {
3484 my ($itemnumber) = @_;
3485 return unless $itemnumber;
3486 my $dbh = C4::Context->dbh;
3487 my $sth = $dbh->prepare(
3488 "DELETE FROM branchtransfers
3490 AND datearrived IS NULL "
3492 return $sth->execute($itemnumber);
3495 =head2 SendCirculationAlert
3497 Send out a C<check-in> or C<checkout> alert using the messaging system.
3505 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
3509 Hashref of information about the item being checked in or out.
3513 Hashref of information about the borrower of the item.
3517 The branchcode from where the checkout or check-in took place.
3523 SendCirculationAlert({
3526 borrower => $borrower,
3532 sub SendCirculationAlert {
3534 my ($type, $item, $borrower, $branch) =
3535 ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
3536 my %message_name = (
3537 CHECKIN => 'Item_Check_in',
3538 CHECKOUT => 'Item_Checkout',
3539 RENEWAL => 'Item_Checkout',
3541 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
3542 borrowernumber => $borrower->{borrowernumber},
3543 message_name => $message_name{$type},
3545 my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues';
3547 my $schema = Koha::Database->new->schema;
3548 my @transports = keys %{ $borrower_preferences->{transports} };
3550 # From the MySQL doc:
3551 # LOCK TABLES is not transaction-safe and implicitly commits any active transaction before attempting to lock the tables.
3552 # If the LOCK/UNLOCK statements are executed from tests, the current transaction will be committed.
3553 # To avoid that we need to guess if this code is execute from tests or not (yes it is a bit hacky)
3554 my $do_not_lock = ( exists $ENV{_} && $ENV{_} =~ m|prove| ) || $ENV{KOHA_TESTING};
3556 for my $mtt (@transports) {
3557 my $letter = C4::Letters::GetPreparedLetter (
3558 module => 'circulation',
3559 letter_code => $type,
3560 branchcode => $branch,
3561 message_transport_type => $mtt,
3562 lang => $borrower->{lang},
3564 $issues_table => $item->{itemnumber},
3565 'items' => $item->{itemnumber},
3566 'biblio' => $item->{biblionumber},
3567 'biblioitems' => $item->{biblionumber},
3568 'borrowers' => $borrower,
3569 'branches' => $branch,
3573 $schema->storage->txn_begin;
3574 C4::Context->dbh->do(q|LOCK TABLE message_queue READ|) unless $do_not_lock;
3575 C4::Context->dbh->do(q|LOCK TABLE message_queue WRITE|) unless $do_not_lock;
3576 my $message = C4::Message->find_last_message($borrower, $type, $mtt);
3577 unless ( $message ) {
3578 C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3579 C4::Message->enqueue($letter, $borrower, $mtt);
3581 $message->append($letter);
3584 C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3585 $schema->storage->txn_commit;
3591 =head2 updateWrongTransfer
3593 $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
3595 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
3599 sub updateWrongTransfer {
3600 my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
3601 my $dbh = C4::Context->dbh;
3602 # first step validate the actual line of transfert .
3605 "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
3607 $sth->execute($FromLibrary,$itemNumber);
3609 # second step create a new line of branchtransfer to the right location .
3610 ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
3612 #third step changing holdingbranch of item
3613 my $item = Koha::Items->find($itemNumber)->holdingbranch($FromLibrary)->store;
3618 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
3620 this function calculates the due date given the start date and configured circulation rules,
3621 checking against the holidays calendar as per the daysmode circulation rule.
3622 C<$startdate> = DateTime object representing start date of loan period (assumed to be today)
3623 C<$itemtype> = itemtype code of item in question
3624 C<$branch> = location whose calendar to use
3625 C<$borrower> = Borrower object
3626 C<$isrenewal> = Boolean: is true if we want to calculate the date due for a renewal. Else is false.
3631 my ( $startdate, $itemtype, $branch, $borrower, $isrenewal ) = @_;
3635 # loanlength now a href
3637 GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
3639 my $length_key = ( $isrenewal and defined $loanlength->{renewalperiod} )
3645 if (ref $startdate ne 'DateTime' ) {
3646 $datedue = dt_from_string($datedue);
3648 $datedue = $startdate->clone;
3651 $datedue = dt_from_string()->truncate( to => 'minute' );
3655 my $daysmode = Koha::CirculationRules->get_effective_daysmode(
3657 categorycode => $borrower->{categorycode},
3658 itemtype => $itemtype,
3659 branchcode => $branch,
3663 # calculate the datedue as normal
3664 if ( $daysmode eq 'Days' )
3665 { # ignoring calendar
3666 if ( $loanlength->{lengthunit} eq 'hours' ) {
3667 $datedue->add( hours => $loanlength->{$length_key} );
3669 $datedue->add( days => $loanlength->{$length_key} );
3670 $datedue->set_hour(23);
3671 $datedue->set_minute(59);
3675 if ($loanlength->{lengthunit} eq 'hours') {
3676 $dur = DateTime::Duration->new( hours => $loanlength->{$length_key});
3679 $dur = DateTime::Duration->new( days => $loanlength->{$length_key});
3681 my $calendar = Koha::Calendar->new( branchcode => $branch, days_mode => $daysmode );
3682 $datedue = $calendar->addDate( $datedue, $dur, $loanlength->{lengthunit} );
3683 if ($loanlength->{lengthunit} eq 'days') {
3684 $datedue->set_hour(23);
3685 $datedue->set_minute(59);
3689 # if Hard Due Dates are used, retrieve them and apply as necessary
3690 my ( $hardduedate, $hardduedatecompare ) =
3691 GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
3692 if ($hardduedate) { # hardduedates are currently dates
3693 $hardduedate->truncate( to => 'minute' );
3694 $hardduedate->set_hour(23);
3695 $hardduedate->set_minute(59);
3696 my $cmp = DateTime->compare( $hardduedate, $datedue );
3698 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
3699 # if the calculated date is before the 'after' Hard Due Date (floor), override
3700 # if the hard due date is set to 'exactly', overrride
3701 if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
3702 $datedue = $hardduedate->clone;
3705 # in all other cases, keep the date due as it is
3709 # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3710 if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3711 my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso', 'floating');
3712 if( $expiry_dt ) { #skip empty expiry date..
3713 $expiry_dt->set( hour => 23, minute => 59);
3714 my $d1= $datedue->clone->set_time_zone('floating');
3715 if ( DateTime->compare( $d1, $expiry_dt ) == 1 ) {
3716 $datedue = $expiry_dt->clone->set_time_zone( C4::Context->tz );
3719 if ( $daysmode ne 'Days' ) {
3720 my $calendar = Koha::Calendar->new( branchcode => $branch, days_mode => $daysmode );
3721 if ( $calendar->is_holiday($datedue) ) {
3722 # Don't return on a closed day
3723 $datedue = $calendar->prev_open_days( $datedue, 1 );
3732 sub CheckValidBarcode{
3734 my $dbh = C4::Context->dbh;
3735 my $query=qq|SELECT count(*)
3739 my $sth = $dbh->prepare($query);
3740 $sth->execute($barcode);
3741 my $exist=$sth->fetchrow ;
3745 =head2 IsBranchTransferAllowed
3747 $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3749 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3751 Deprecated in favor of Koha::Item::Transfer::Limits->find/search and
3752 Koha::Item->can_be_transferred.
3756 sub IsBranchTransferAllowed {
3757 my ( $toBranch, $fromBranch, $code ) = @_;
3759 if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3761 my $limitType = C4::Context->preference("BranchTransferLimitsType");
3762 my $dbh = C4::Context->dbh;
3764 my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3765 $sth->execute( $toBranch, $fromBranch, $code );
3766 my $limit = $sth->fetchrow_hashref();
3768 ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3769 if ( $limit->{'limitId'} ) {
3776 =head2 CreateBranchTransferLimit
3778 CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3780 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3782 Deprecated in favor of Koha::Item::Transfer::Limit->new.
3786 sub CreateBranchTransferLimit {
3787 my ( $toBranch, $fromBranch, $code ) = @_;
3788 return unless defined($toBranch) && defined($fromBranch);
3789 my $limitType = C4::Context->preference("BranchTransferLimitsType");
3791 my $dbh = C4::Context->dbh;
3793 my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3794 return $sth->execute( $code, $toBranch, $fromBranch );
3797 =head2 DeleteBranchTransferLimits
3799 my $result = DeleteBranchTransferLimits($frombranch);
3801 Deletes all the library transfer limits for one library. Returns the
3802 number of limits deleted, 0e0 if no limits were deleted, or undef if
3803 no arguments are supplied.
3805 Deprecated in favor of Koha::Item::Transfer::Limits->search({
3806 fromBranch => $fromBranch
3811 sub DeleteBranchTransferLimits {
3813 return unless defined $branch;
3814 my $dbh = C4::Context->dbh;
3815 my $sth = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3816 return $sth->execute($branch);
3820 my ( $borrowernumber, $itemnum ) = @_;
3821 MarkIssueReturned( $borrowernumber, $itemnum );
3826 LostItem( $itemnumber, $mark_lost_from, $force_mark_returned, [$params] );
3828 The final optional parameter, C<$params>, expected to contain
3829 'skip_record_index' key, which relayed down to Koha::Item/store,
3830 there it prevents calling of ModZebra index_records,
3831 which takes most of the time in batch adds/deletes: index_records better
3832 to be called later in C<additem.pl> after the whole loop.
3835 skip_record_index => 1|0
3840 my ($itemnumber, $mark_lost_from, $force_mark_returned, $params) = @_;
3842 unless ( $mark_lost_from ) {
3843 # Temporary check to avoid regressions
3844 die q|LostItem called without $mark_lost_from, check the API.|;
3848 if ( $force_mark_returned ) {
3851 my $pref = C4::Context->preference('MarkLostItemsAsReturned') // q{};
3852 $mark_returned = ( $pref =~ m|$mark_lost_from| );
3855 my $dbh = C4::Context->dbh();
3856 my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title
3858 JOIN items USING (itemnumber)
3859 JOIN biblio USING (biblionumber)
3860 WHERE issues.itemnumber=?");
3861 $sth->execute($itemnumber);
3862 my $issues=$sth->fetchrow_hashref();
3864 # If a borrower lost the item, add a replacement cost to the their record
3865 if ( my $borrowernumber = $issues->{borrowernumber} ){
3866 my $patron = Koha::Patrons->find( $borrowernumber );
3868 my $fix = _FixOverduesOnReturn($borrowernumber, $itemnumber, C4::Context->preference('WhenLostForgiveFine'), 'LOST');
3869 defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $itemnumber...) failed!"; # zero is OK, check defined
3871 if (C4::Context->preference('WhenLostChargeReplacementFee')){
3872 C4::Accounts::chargelostitem(
3875 $issues->{'replacementprice'},
3876 sprintf( "%s %s %s",
3877 $issues->{'title'} || q{},
3878 $issues->{'barcode'} || q{},
3879 $issues->{'itemcallnumber'} || q{},
3882 #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3883 #warn " $issues->{'borrowernumber'} / $itemnumber ";
3886 MarkIssueReturned($borrowernumber,$itemnumber,undef,$patron->privacy) if $mark_returned;
3889 #When item is marked lost automatically cancel its outstanding transfers and set items holdingbranch to the transfer source branch (frombranch)
3890 if (my ( $datesent,$frombranch,$tobranch ) = GetTransfers($itemnumber)) {
3891 Koha::Items->find($itemnumber)->holdingbranch($frombranch)->store({ skip_record_index => $params->{skip_record_index} });
3893 my $transferdeleted = DeleteTransfer($itemnumber);
3896 sub GetOfflineOperations {
3897 my $dbh = C4::Context->dbh;
3898 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3899 $sth->execute(C4::Context->userenv->{'branch'});
3900 my $results = $sth->fetchall_arrayref({});
3904 sub GetOfflineOperation {
3905 my $operationid = shift;
3906 return unless $operationid;
3907 my $dbh = C4::Context->dbh;
3908 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3909 $sth->execute( $operationid );
3910 return $sth->fetchrow_hashref;
3913 sub AddOfflineOperation {
3914 my ( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount ) = @_;
3915 my $dbh = C4::Context->dbh;
3916 my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber, amount) VALUES(?,?,?,?,?,?,?)");
3917 $sth->execute( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount );
3921 sub DeleteOfflineOperation {
3922 my $dbh = C4::Context->dbh;
3923 my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3924 $sth->execute( shift );
3928 sub ProcessOfflineOperation {
3929 my $operation = shift;
3932 if ( $operation->{action} eq 'return' ) {
3933 $report = ProcessOfflineReturn( $operation );
3934 } elsif ( $operation->{action} eq 'issue' ) {
3935 $report = ProcessOfflineIssue( $operation );
3936 } elsif ( $operation->{action} eq 'payment' ) {
3937 $report = ProcessOfflinePayment( $operation );
3940 DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3945 sub ProcessOfflineReturn {
3946 my $operation = shift;
3948 my $item = Koha::Items->find({barcode => $operation->{barcode}});
3951 my $itemnumber = $item->itemnumber;
3952 my $issue = GetOpenIssue( $itemnumber );
3954 my $leave_item_lost = C4::Context->preference("BlockReturnOfLostItems") ? 1 : 0;
3955 ModDateLastSeen( $itemnumber, $leave_item_lost );
3957 $issue->{borrowernumber},
3959 $operation->{timestamp},
3962 $item->onloan(undef);
3963 $item->store({ log_action => 0 });
3966 return "Item not issued.";
3969 return "Item not found.";
3973 sub ProcessOfflineIssue {
3974 my $operation = shift;
3976 my $patron = Koha::Patrons->find( { cardnumber => $operation->{cardnumber} } );
3979 my $item = Koha::Items->find({ barcode => $operation->{barcode} });
3981 return "Barcode not found.";
3983 my $itemnumber = $item->itemnumber;
3984 my $issue = GetOpenIssue( $itemnumber );
3986 if ( $issue and ( $issue->{borrowernumber} ne $patron->borrowernumber ) ) { # Item already issued to another patron mark it returned
3988 $issue->{borrowernumber},
3990 $operation->{timestamp},
3995 $operation->{'barcode'},
3998 $operation->{timestamp},
4003 return "Borrower not found.";
4007 sub ProcessOfflinePayment {
4008 my $operation = shift;
4010 my $patron = Koha::Patrons->find({ cardnumber => $operation->{cardnumber} });
4012 $patron->account->pay(
4014 amount => $operation->{amount},
4015 library_id => $operation->{branchcode},
4025 TransferSlip($user_branch, $itemnumber, $barcode, $to_branch)
4027 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
4032 my ($branch, $itemnumber, $barcode, $to_branch) = @_;
4036 ? Koha::Items->find($itemnumber)
4037 : Koha::Items->find( { barcode => $barcode } );
4041 return C4::Letters::GetPreparedLetter (
4042 module => 'circulation',
4043 letter_code => 'TRANSFERSLIP',
4044 branchcode => $branch,
4046 'branches' => $to_branch,
4047 'biblio' => $item->biblionumber,
4048 'items' => $item->unblessed,
4053 =head2 CheckIfIssuedToPatron
4055 CheckIfIssuedToPatron($borrowernumber, $biblionumber)
4057 Return 1 if any record item is issued to patron, otherwise return 0
4061 sub CheckIfIssuedToPatron {
4062 my ($borrowernumber, $biblionumber) = @_;
4064 my $dbh = C4::Context->dbh;
4066 SELECT COUNT(*) FROM issues
4067 LEFT JOIN items ON items.itemnumber = issues.itemnumber
4068 WHERE items.biblionumber = ?
4069 AND issues.borrowernumber = ?
4071 my $is_issued = $dbh->selectrow_array($query, {}, $biblionumber, $borrowernumber );
4072 return 1 if $is_issued;
4078 IsItemIssued( $itemnumber )
4080 Return 1 if the item is on loan, otherwise return 0
4085 my $itemnumber = shift;
4086 my $dbh = C4::Context->dbh;
4087 my $sth = $dbh->prepare(q{
4090 WHERE itemnumber = ?
4092 $sth->execute($itemnumber);
4093 return $sth->fetchrow;
4096 =head2 GetAgeRestriction
4098 my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions, $borrower);
4099 my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions);
4101 if($daysToAgeRestriction <= 0) { #Borrower is allowed to access this material, as they are older or as old as the agerestriction }
4102 if($daysToAgeRestriction > 0) { #Borrower is this many days from meeting the agerestriction }
4104 @PARAM1 the koha.biblioitems.agerestriction value, like K18, PEGI 13, ...
4105 @PARAM2 a borrower-object with koha.borrowers.dateofbirth. (OPTIONAL)
4106 @RETURNS The age restriction age in years and the days to fulfill the age restriction for the given borrower.
4107 Negative days mean the borrower has gone past the age restriction age.
4111 sub GetAgeRestriction {
4112 my ($record_restrictions, $borrower) = @_;
4113 my $markers = C4::Context->preference('AgeRestrictionMarker');
4115 return unless $record_restrictions;
4116 # Split $record_restrictions to something like FSK 16 or PEGI 6
4117 my @values = split ' ', uc($record_restrictions);
4118 return unless @values;
4120 # Search first occurrence of one of the markers
4121 my @markers = split /\|/, uc($markers);
4122 return unless @markers;
4125 my $restriction_year = 0;
4126 for my $value (@values) {
4128 for my $marker (@markers) {
4129 $marker =~ s/^\s+//; #remove leading spaces
4130 $marker =~ s/\s+$//; #remove trailing spaces
4131 if ( $marker eq $value ) {
4132 if ( $index <= $#values ) {
4133 $restriction_year += $values[$index];
4137 elsif ( $value =~ /^\Q$marker\E(\d+)$/ ) {
4139 # Perhaps it is something like "K16" (as in Finland)
4140 $restriction_year += $1;
4144 last if ( $restriction_year > 0 );
4147 #Check if the borrower is age restricted for this material and for how long.
4148 if ($restriction_year && $borrower) {
4149 if ( $borrower->{'dateofbirth'} ) {
4150 my @alloweddate = split /-/, $borrower->{'dateofbirth'};
4151 $alloweddate[0] += $restriction_year;
4153 #Prevent runime eror on leap year (invalid date)
4154 if ( ( $alloweddate[1] == 2 ) && ( $alloweddate[2] == 29 ) ) {
4155 $alloweddate[2] = 28;
4158 #Get how many days the borrower has to reach the age restriction
4159 my @Today = split /-/, dt_from_string()->ymd();
4160 my $daysToAgeRestriction = Date_to_Days(@alloweddate) - Date_to_Days(@Today);
4161 #Negative days means the borrower went past the age restriction age
4162 return ($restriction_year, $daysToAgeRestriction);
4166 return ($restriction_year);
4170 =head2 GetPendingOnSiteCheckouts
4174 sub GetPendingOnSiteCheckouts {
4175 my $dbh = C4::Context->dbh;
4176 return $dbh->selectall_arrayref(q|
4182 items.itemcallnumber,
4186 issues.date_due < NOW() AS is_overdue,
4189 borrowers.firstname,
4191 borrowers.cardnumber,
4192 borrowers.borrowernumber
4194 LEFT JOIN issues ON items.itemnumber = issues.itemnumber
4195 LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
4196 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
4197 WHERE issues.onsite_checkout = 1
4198 |, { Slice => {} } );
4204 my ($count, $branch, $itemtype, $ccode, $newness)
4205 = @$params{qw(count branch itemtype ccode newness)};
4207 my $dbh = C4::Context->dbh;
4210 SELECT b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4211 bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4212 i.ccode, SUM(i.issues) AS count
4214 LEFT JOIN items i ON (i.biblionumber = b.biblionumber)
4215 LEFT JOIN biblioitems bi ON (bi.biblionumber = b.biblionumber)
4218 my (@where_strs, @where_args);
4221 push @where_strs, 'i.homebranch = ?';
4222 push @where_args, $branch;
4225 if (C4::Context->preference('item-level_itypes')){
4226 push @where_strs, 'i.itype = ?';
4227 push @where_args, $itemtype;
4229 push @where_strs, 'bi.itemtype = ?';
4230 push @where_args, $itemtype;
4234 push @where_strs, 'i.ccode = ?';
4235 push @where_args, $ccode;
4238 push @where_strs, 'TO_DAYS(NOW()) - TO_DAYS(b.datecreated) <= ?';
4239 push @where_args, $newness;
4243 $query .= 'WHERE ' . join(' AND ', @where_strs);
4247 GROUP BY b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4248 bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4253 $query .= q{ ) xxx WHERE count > 0 };
4254 $count = int($count);
4256 $query .= "LIMIT $count";
4259 my $rows = $dbh->selectall_arrayref($query, { Slice => {} }, @where_args);
4264 =head2 Internal methods
4268 sub _CalculateAndUpdateFine {
4271 my $borrower = $params->{borrower};
4272 my $item = $params->{item};
4273 my $issue = $params->{issue};
4274 my $return_date = $params->{return_date};
4276 unless ($borrower) { carp "No borrower passed in!" && return; }
4277 unless ($item) { carp "No item passed in!" && return; }
4278 unless ($issue) { carp "No issue passed in!" && return; }
4280 my $datedue = dt_from_string( $issue->date_due );
4282 # we only need to calculate and change the fines if we want to do that on return
4283 # Should be on for hourly loans
4284 my $control = C4::Context->preference('CircControl');
4285 my $control_branchcode =
4286 ( $control eq 'ItemHomeLibrary' ) ? $item->{homebranch}
4287 : ( $control eq 'PatronLibrary' ) ? $borrower->{branchcode}
4288 : $issue->branchcode;
4290 my $date_returned = $return_date ? $return_date : dt_from_string();
4292 my ( $amount, $unitcounttotal, $unitcount ) =
4293 C4::Overdues::CalcFine( $item, $borrower->{categorycode}, $control_branchcode, $datedue, $date_returned );
4295 if ( C4::Context->preference('finesMode') eq 'production' ) {
4296 if ( $amount > 0 ) {
4297 C4::Overdues::UpdateFine({
4298 issue_id => $issue->issue_id,
4299 itemnumber => $issue->itemnumber,
4300 borrowernumber => $issue->borrowernumber,
4302 due => output_pref($datedue),
4305 elsif ($return_date) {
4307 # Backdated returns may have fines that shouldn't exist,
4308 # so in this case, we need to drop those fines to 0
4310 C4::Overdues::UpdateFine({
4311 issue_id => $issue->issue_id,
4312 itemnumber => $issue->itemnumber,
4313 borrowernumber => $issue->borrowernumber,
4315 due => output_pref($datedue),
4321 sub _item_denied_renewal {
4324 my $item = $params->{item};
4325 return unless $item;
4327 my $denyingrules = Koha::Config::SysPrefs->find('ItemsDeniedRenewal')->get_yaml_pref_hash();
4328 return unless $denyingrules;
4329 foreach my $field (keys %$denyingrules) {
4330 my $val = $item->$field;
4331 if( !defined $val) {
4332 if ( any { !defined $_ } @{$denyingrules->{$field}} ){
4335 } elsif (any { defined($_) && $val eq $_ } @{$denyingrules->{$field}}) {
4336 # If the results matches the values in the syspref
4337 # We return true if match found
4350 Koha Development Team <http://koha-community.org/>