Bug 18654 - Translatability: Get rid of tt directives starting with [%% in translatio...
[koha.git] / C4 / Circulation.pm
blobe4fee8f1faf8fc4c1b03f908e86a25d5d11b90d9
1 package C4::Circulation;
3 # Copyright 2000-2002 Katipo Communications
4 # copyright 2010 BibLibre
6 # This file is part of Koha.
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use strict;
23 #use warnings; FIXME - Bug 2505
24 use DateTime;
25 use Koha::DateUtils;
26 use C4::Context;
27 use C4::Stats;
28 use C4::Reserves;
29 use C4::Biblio;
30 use C4::Items;
31 use C4::Members;
32 use C4::Accounts;
33 use C4::ItemCirculationAlertPreference;
34 use C4::Message;
35 use C4::Debug;
36 use C4::Log; # logaction
37 use C4::Overdues qw(CalcFine UpdateFine get_chargeable_units);
38 use C4::RotatingCollections qw(GetCollectionItemBranches);
39 use Algorithm::CheckDigits;
41 use Data::Dumper;
42 use Koha::Account;
43 use Koha::AuthorisedValues;
44 use Koha::Biblioitems;
45 use Koha::DateUtils;
46 use Koha::Calendar;
47 use Koha::Checkouts;
48 use Koha::IssuingRules;
49 use Koha::Items;
50 use Koha::Patrons;
51 use Koha::Patron::Debarments;
52 use Koha::Database;
53 use Koha::Libraries;
54 use Koha::Holds;
55 use Koha::RefundLostItemFeeRule;
56 use Koha::RefundLostItemFeeRules;
57 use Carp;
58 use List::MoreUtils qw( uniq );
59 use Scalar::Util qw( looks_like_number );
60 use Date::Calc qw(
61 Today
62 Today_and_Now
63 Add_Delta_YM
64 Add_Delta_DHMS
65 Date_to_Days
66 Day_of_Week
67 Add_Delta_Days
69 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
71 BEGIN {
72 require Exporter;
73 @ISA = qw(Exporter);
75 # FIXME subs that should probably be elsewhere
76 push @EXPORT, qw(
77 &barcodedecode
78 &LostItem
79 &ReturnLostItem
80 &GetPendingOnSiteCheckouts
83 # subs to deal with issuing a book
84 push @EXPORT, qw(
85 &CanBookBeIssued
86 &CanBookBeRenewed
87 &AddIssue
88 &AddRenewal
89 &GetRenewCount
90 &GetSoonestRenewDate
91 &GetLatestAutoRenewDate
92 &GetIssuingCharges
93 &GetBranchBorrowerCircRule
94 &GetBranchItemRule
95 &GetBiblioIssues
96 &GetOpenIssue
97 &CheckIfIssuedToPatron
98 &IsItemIssued
99 GetTopIssues
102 # subs to deal with returns
103 push @EXPORT, qw(
104 &AddReturn
105 &MarkIssueReturned
108 # subs to deal with transfers
109 push @EXPORT, qw(
110 &transferbook
111 &GetTransfers
112 &GetTransfersFromTo
113 &updateWrongTransfer
114 &DeleteTransfer
115 &IsBranchTransferAllowed
116 &CreateBranchTransferLimit
117 &DeleteBranchTransferLimits
118 &TransferSlip
121 # subs to deal with offline circulation
122 push @EXPORT, qw(
123 &GetOfflineOperations
124 &GetOfflineOperation
125 &AddOfflineOperation
126 &DeleteOfflineOperation
127 &ProcessOfflineOperation
131 =head1 NAME
133 C4::Circulation - Koha circulation module
135 =head1 SYNOPSIS
137 use C4::Circulation;
139 =head1 DESCRIPTION
141 The functions in this module deal with circulation, issues, and
142 returns, as well as general information about the library.
143 Also deals with inventory.
145 =head1 FUNCTIONS
147 =head2 barcodedecode
149 $str = &barcodedecode($barcode, [$filter]);
151 Generic filter function for barcode string.
152 Called on every circ if the System Pref itemBarcodeInputFilter is set.
153 Will do some manipulation of the barcode for systems that deliver a barcode
154 to circulation.pl that differs from the barcode stored for the item.
155 For proper functioning of this filter, calling the function on the
156 correct barcode string (items.barcode) should return an unaltered barcode.
158 The optional $filter argument is to allow for testing or explicit
159 behavior that ignores the System Pref. Valid values are the same as the
160 System Pref options.
162 =cut
164 # FIXME -- the &decode fcn below should be wrapped into this one.
165 # FIXME -- these plugins should be moved out of Circulation.pm
167 sub barcodedecode {
168 my ($barcode, $filter) = @_;
169 my $branch = C4::Context::mybranch();
170 $filter = C4::Context->preference('itemBarcodeInputFilter') unless $filter;
171 $filter or return $barcode; # ensure filter is defined, else return untouched barcode
172 if ($filter eq 'whitespace') {
173 $barcode =~ s/\s//g;
174 } elsif ($filter eq 'cuecat') {
175 chomp($barcode);
176 my @fields = split( /\./, $barcode );
177 my @results = map( decode($_), @fields[ 1 .. $#fields ] );
178 ($#results == 2) and return $results[2];
179 } elsif ($filter eq 'T-prefix') {
180 if ($barcode =~ /^[Tt](\d)/) {
181 (defined($1) and $1 eq '0') and return $barcode;
182 $barcode = substr($barcode, 2) + 0; # FIXME: probably should be substr($barcode, 1)
184 return sprintf("T%07d", $barcode);
185 # FIXME: $barcode could be "T1", causing warning: substr outside of string
186 # Why drop the nonzero digit after the T?
187 # Why pass non-digits (or empty string) to "T%07d"?
188 } elsif ($filter eq 'libsuite8') {
189 unless($barcode =~ m/^($branch)-/i){ #if barcode starts with branch code its in Koha style. Skip it.
190 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
191 $barcode =~ s/^[0]*(\d+)$/$branch-b-$1/i;
192 }else{
193 $barcode =~ s/^(\D+)[0]*(\d+)$/$branch-$1-$2/i;
196 } elsif ($filter eq 'EAN13') {
197 my $ean = CheckDigits('ean');
198 if ( $ean->is_valid($barcode) ) {
199 #$barcode = sprintf('%013d',$barcode); # this doesn't work on 32-bit systems
200 $barcode = '0' x ( 13 - length($barcode) ) . $barcode;
201 } else {
202 warn "# [$barcode] not valid EAN-13/UPC-A\n";
205 return $barcode; # return barcode, modified or not
208 =head2 decode
210 $str = &decode($chunk);
212 Decodes a segment of a string emitted by a CueCat barcode scanner and
213 returns it.
215 FIXME: Should be replaced with Barcode::Cuecat from CPAN
216 or Javascript based decoding on the client side.
218 =cut
220 sub decode {
221 my ($encoded) = @_;
222 my $seq =
223 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-';
224 my @s = map { index( $seq, $_ ); } split( //, $encoded );
225 my $l = ( $#s + 1 ) % 4;
226 if ($l) {
227 if ( $l == 1 ) {
228 # warn "Error: Cuecat decode parsing failed!";
229 return;
231 $l = 4 - $l;
232 $#s += $l;
234 my $r = '';
235 while ( $#s >= 0 ) {
236 my $n = ( ( $s[0] << 6 | $s[1] ) << 6 | $s[2] ) << 6 | $s[3];
237 $r .=
238 chr( ( $n >> 16 ) ^ 67 )
239 .chr( ( $n >> 8 & 255 ) ^ 67 )
240 .chr( ( $n & 255 ) ^ 67 );
241 @s = @s[ 4 .. $#s ];
243 $r = substr( $r, 0, length($r) - $l );
244 return $r;
247 =head2 transferbook
249 ($dotransfer, $messages, $iteminformation) = &transferbook($newbranch,
250 $barcode, $ignore_reserves);
252 Transfers an item to a new branch. If the item is currently on loan, it is automatically returned before the actual transfer.
254 C<$newbranch> is the code for the branch to which the item should be transferred.
256 C<$barcode> is the barcode of the item to be transferred.
258 If C<$ignore_reserves> is true, C<&transferbook> ignores reserves.
259 Otherwise, if an item is reserved, the transfer fails.
261 Returns three values:
263 =over
265 =item $dotransfer
267 is true if the transfer was successful.
269 =item $messages
271 is a reference-to-hash which may have any of the following keys:
273 =over
275 =item C<BadBarcode>
277 There is no item in the catalog with the given barcode. The value is C<$barcode>.
279 =item C<IsPermanent>
281 The item's home branch is permanent. This doesn't prevent the item from being transferred, though. The value is the code of the item's home branch.
283 =item C<DestinationEqualsHolding>
285 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.
287 =item C<WasReturned>
289 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.
291 =item C<ResFound>
293 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>.
295 =item C<WasTransferred>
297 The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored.
299 =back
301 =back
303 =cut
305 sub transferbook {
306 my ( $tbr, $barcode, $ignoreRs ) = @_;
307 my $messages;
308 my $dotransfer = 1;
309 my $item = Koha::Items->find( { barcode => $barcode } );
311 # bad barcode..
312 unless ( $item ) {
313 $messages->{'BadBarcode'} = $barcode;
314 $dotransfer = 0;
317 my $itemnumber = $item->itemnumber;
318 my $issue = GetOpenIssue($itemnumber);
319 # get branches of book...
320 my $hbr = $item->homebranch;
321 my $fbr = $item->holdingbranch;
323 # if using Branch Transfer Limits
324 if ( C4::Context->preference("UseBranchTransferLimits") == 1 ) {
325 my $code = C4::Context->preference("BranchTransferLimitsType") eq 'ccode' ? $item->ccode : $item->biblio->biblioitem->itemtype; # BranchTransferLimitsType is 'ccode' or 'itemtype'
326 if ( C4::Context->preference("item-level_itypes") && C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ) {
327 if ( ! IsBranchTransferAllowed( $tbr, $fbr, $item->itype ) ) {
328 $messages->{'NotAllowed'} = $tbr . "::" . $item->itype;
329 $dotransfer = 0;
331 } elsif ( ! IsBranchTransferAllowed( $tbr, $fbr, $code ) ) {
332 $messages->{'NotAllowed'} = $tbr . "::" . $code;
333 $dotransfer = 0;
337 # if is permanent...
338 # FIXME Is this still used by someone?
339 # See other FIXME in AddReturn
340 my $library = Koha::Libraries->find($hbr);
341 if ( $library and $library->get_categories->search({'me.categorycode' => 'PE'})->count ) {
342 $messages->{'IsPermanent'} = $hbr;
343 $dotransfer = 0;
346 # can't transfer book if is already there....
347 if ( $fbr eq $tbr ) {
348 $messages->{'DestinationEqualsHolding'} = 1;
349 $dotransfer = 0;
352 # check if it is still issued to someone, return it...
353 if ( $issue ) {
354 AddReturn( $barcode, $fbr );
355 $messages->{'WasReturned'} = $issue->borrowernumber;
358 # find reserves.....
359 # That'll save a database query.
360 my ( $resfound, $resrec, undef ) =
361 CheckReserves( $itemnumber );
362 if ( $resfound and not $ignoreRs ) {
363 $resrec->{'ResFound'} = $resfound;
365 # $messages->{'ResFound'} = $resrec;
366 $dotransfer = 1;
369 #actually do the transfer....
370 if ($dotransfer) {
371 ModItemTransfer( $itemnumber, $fbr, $tbr );
373 # don't need to update MARC anymore, we do it in batch now
374 $messages->{'WasTransfered'} = 1;
377 ModDateLastSeen( $itemnumber );
378 return ( $dotransfer, $messages );
382 sub TooMany {
383 my $borrower = shift;
384 my $biblionumber = shift;
385 my $item = shift;
386 my $params = shift;
387 my $onsite_checkout = $params->{onsite_checkout} || 0;
388 my $switch_onsite_checkout = $params->{switch_onsite_checkout} || 0;
389 my $cat_borrower = $borrower->{'categorycode'};
390 my $dbh = C4::Context->dbh;
391 my $branch;
392 # Get which branchcode we need
393 $branch = _GetCircControlBranch($item,$borrower);
394 my $type = (C4::Context->preference('item-level_itypes'))
395 ? $item->{'itype'} # item-level
396 : $item->{'itemtype'}; # biblio-level
398 # given branch, patron category, and item type, determine
399 # applicable issuing rule
400 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
401 { categorycode => $cat_borrower,
402 itemtype => $type,
403 branchcode => $branch
408 # if a rule is found and has a loan limit set, count
409 # how many loans the patron already has that meet that
410 # rule
411 if (defined($issuing_rule) and defined($issuing_rule->maxissueqty)) {
412 my @bind_params;
413 my $count_query = q|
414 SELECT COUNT(*) AS total, COALESCE(SUM(onsite_checkout), 0) AS onsite_checkouts
415 FROM issues
416 JOIN items USING (itemnumber)
419 my $rule_itemtype = $issuing_rule->itemtype;
420 if ($rule_itemtype eq "*") {
421 # matching rule has the default item type, so count only
422 # those existing loans that don't fall under a more
423 # specific rule
424 if (C4::Context->preference('item-level_itypes')) {
425 $count_query .= " WHERE items.itype NOT IN (
426 SELECT itemtype FROM issuingrules
427 WHERE branchcode = ?
428 AND (categorycode = ? OR categorycode = ?)
429 AND itemtype <> '*'
430 ) ";
431 } else {
432 $count_query .= " JOIN biblioitems USING (biblionumber)
433 WHERE biblioitems.itemtype NOT IN (
434 SELECT itemtype FROM issuingrules
435 WHERE branchcode = ?
436 AND (categorycode = ? OR categorycode = ?)
437 AND itemtype <> '*'
438 ) ";
440 push @bind_params, $issuing_rule->branchcode;
441 push @bind_params, $issuing_rule->categorycode;
442 push @bind_params, $cat_borrower;
443 } else {
444 # rule has specific item type, so count loans of that
445 # specific item type
446 if (C4::Context->preference('item-level_itypes')) {
447 $count_query .= " WHERE items.itype = ? ";
448 } else {
449 $count_query .= " JOIN biblioitems USING (biblionumber)
450 WHERE biblioitems.itemtype= ? ";
452 push @bind_params, $type;
455 $count_query .= " AND borrowernumber = ? ";
456 push @bind_params, $borrower->{'borrowernumber'};
457 my $rule_branch = $issuing_rule->branchcode;
458 if ($rule_branch ne "*") {
459 if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
460 $count_query .= " AND issues.branchcode = ? ";
461 push @bind_params, $branch;
462 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
463 ; # if branch is the patron's home branch, then count all loans by patron
464 } else {
465 $count_query .= " AND items.homebranch = ? ";
466 push @bind_params, $branch;
470 my ( $checkout_count, $onsite_checkout_count ) = $dbh->selectrow_array( $count_query, {}, @bind_params );
472 my $max_checkouts_allowed = $issuing_rule->maxissueqty;
473 my $max_onsite_checkouts_allowed = $issuing_rule->maxonsiteissueqty;
475 if ( $onsite_checkout ) {
476 if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed ) {
477 return {
478 reason => 'TOO_MANY_ONSITE_CHECKOUTS',
479 count => $onsite_checkout_count,
480 max_allowed => $max_onsite_checkouts_allowed,
484 if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) {
485 my $delta = $switch_onsite_checkout ? 1 : 0;
486 if ( $checkout_count >= $max_checkouts_allowed + $delta ) {
487 return {
488 reason => 'TOO_MANY_CHECKOUTS',
489 count => $checkout_count,
490 max_allowed => $max_checkouts_allowed,
493 } elsif ( not $onsite_checkout ) {
494 if ( $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed ) {
495 return {
496 reason => 'TOO_MANY_CHECKOUTS',
497 count => $checkout_count - $onsite_checkout_count,
498 max_allowed => $max_checkouts_allowed,
504 # Now count total loans against the limit for the branch
505 my $branch_borrower_circ_rule = GetBranchBorrowerCircRule($branch, $cat_borrower);
506 if (defined($branch_borrower_circ_rule->{maxissueqty})) {
507 my @bind_params = ();
508 my $branch_count_query = q|
509 SELECT COUNT(*) AS total, COALESCE(SUM(onsite_checkout), 0) AS onsite_checkouts
510 FROM issues
511 JOIN items USING (itemnumber)
512 WHERE borrowernumber = ?
514 push @bind_params, $borrower->{borrowernumber};
516 if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
517 $branch_count_query .= " AND issues.branchcode = ? ";
518 push @bind_params, $branch;
519 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
520 ; # if branch is the patron's home branch, then count all loans by patron
521 } else {
522 $branch_count_query .= " AND items.homebranch = ? ";
523 push @bind_params, $branch;
525 my ( $checkout_count, $onsite_checkout_count ) = $dbh->selectrow_array( $branch_count_query, {}, @bind_params );
526 my $max_checkouts_allowed = $branch_borrower_circ_rule->{maxissueqty};
527 my $max_onsite_checkouts_allowed = $branch_borrower_circ_rule->{maxonsiteissueqty};
529 if ( $onsite_checkout ) {
530 if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed ) {
531 return {
532 reason => 'TOO_MANY_ONSITE_CHECKOUTS',
533 count => $onsite_checkout_count,
534 max_allowed => $max_onsite_checkouts_allowed,
538 if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) {
539 my $delta = $switch_onsite_checkout ? 1 : 0;
540 if ( $checkout_count >= $max_checkouts_allowed + $delta ) {
541 return {
542 reason => 'TOO_MANY_CHECKOUTS',
543 count => $checkout_count,
544 max_allowed => $max_checkouts_allowed,
547 } elsif ( not $onsite_checkout ) {
548 if ( $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed ) {
549 return {
550 reason => 'TOO_MANY_CHECKOUTS',
551 count => $checkout_count - $onsite_checkout_count,
552 max_allowed => $max_checkouts_allowed,
558 if ( not defined( $issuing_rule ) and not defined($branch_borrower_circ_rule->{maxissueqty}) ) {
559 return { reason => 'NO_RULE_DEFINED', max_allowed => 0 };
562 # OK, the patron can issue !!!
563 return;
566 =head2 CanBookBeIssued
568 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $borrower,
569 $barcode, $duedate, $inprocess, $ignore_reserves, $params );
571 Check if a book can be issued.
573 C<$issuingimpossible> and C<$needsconfirmation> are some hashref.
575 =over 4
577 =item C<$borrower> hash with borrower informations (from Koha::Patron->unblessed)
579 =item C<$barcode> is the bar code of the book being issued.
581 =item C<$duedates> is a DateTime object.
583 =item C<$inprocess> boolean switch
585 =item C<$ignore_reserves> boolean switch
587 =item C<$params> Hashref of additional parameters
589 Available keys:
590 override_high_holds - Ignore high holds
591 onsite_checkout - Checkout is an onsite checkout that will not leave the library
593 =back
595 Returns :
597 =over 4
599 =item C<$issuingimpossible> a reference to a hash. It contains reasons why issuing is impossible.
600 Possible values are :
602 =back
604 =head3 INVALID_DATE
606 sticky due date is invalid
608 =head3 GNA
610 borrower gone with no address
612 =head3 CARD_LOST
614 borrower declared it's card lost
616 =head3 DEBARRED
618 borrower debarred
620 =head3 UNKNOWN_BARCODE
622 barcode unknown
624 =head3 NOT_FOR_LOAN
626 item is not for loan
628 =head3 WTHDRAWN
630 item withdrawn.
632 =head3 RESTRICTED
634 item is restricted (set by ??)
636 C<$needsconfirmation> a reference to a hash. It contains reasons why the loan
637 could be prevented, but ones that can be overriden by the operator.
639 Possible values are :
641 =head3 DEBT
643 borrower has debts.
645 =head3 RENEW_ISSUE
647 renewing, not issuing
649 =head3 ISSUED_TO_ANOTHER
651 issued to someone else.
653 =head3 RESERVED
655 reserved for someone else.
657 =head3 INVALID_DATE
659 sticky due date is invalid or due date in the past
661 =head3 TOO_MANY
663 if the borrower borrows to much things
665 =cut
667 sub CanBookBeIssued {
668 my ( $borrower, $barcode, $duedate, $inprocess, $ignore_reserves, $params ) = @_;
669 my %needsconfirmation; # filled with problems that needs confirmations
670 my %issuingimpossible; # filled with problems that causes the issue to be IMPOSSIBLE
671 my %alerts; # filled with messages that shouldn't stop issuing, but the librarian should be aware of.
672 my %messages; # filled with information messages that should be displayed.
674 my $onsite_checkout = $params->{onsite_checkout} || 0;
675 my $override_high_holds = $params->{override_high_holds} || 0;
677 my $item = GetItem(undef, $barcode );
678 my $issue = Koha::Checkouts->find( { itemnumber => $item->{itemnumber} } );
679 my $biblioitem = GetBiblioItemData($item->{biblioitemnumber});
680 $item->{'itemtype'}=$item->{'itype'};
681 my $dbh = C4::Context->dbh;
683 # MANDATORY CHECKS - unless item exists, nothing else matters
684 unless ( $item->{barcode} ) {
685 $issuingimpossible{UNKNOWN_BARCODE} = 1;
687 return ( \%issuingimpossible, \%needsconfirmation ) if %issuingimpossible;
690 # DUE DATE is OK ? -- should already have checked.
692 if ($duedate && ref $duedate ne 'DateTime') {
693 $duedate = dt_from_string($duedate);
695 my $now = DateTime->now( time_zone => C4::Context->tz() );
696 unless ( $duedate ) {
697 my $issuedate = $now->clone();
699 my $branch = _GetCircControlBranch($item,$borrower);
700 my $itype = ( C4::Context->preference('item-level_itypes') ) ? $item->{'itype'} : $biblioitem->{'itemtype'};
701 $duedate = CalcDateDue( $issuedate, $itype, $branch, $borrower );
703 # Offline circ calls AddIssue directly, doesn't run through here
704 # So issuingimpossible should be ok.
706 if ($duedate) {
707 my $today = $now->clone();
708 $today->truncate( to => 'minute');
709 if (DateTime->compare($duedate,$today) == -1 ) { # duedate cannot be before now
710 $needsconfirmation{INVALID_DATE} = output_pref($duedate);
712 } else {
713 $issuingimpossible{INVALID_DATE} = output_pref($duedate);
717 # BORROWER STATUS
719 if ( $borrower->{'category_type'} eq 'X' && ( $item->{barcode} )) {
720 # stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1 .
721 &UpdateStats({
722 branch => C4::Context->userenv->{'branch'},
723 type => 'localuse',
724 itemnumber => $item->{'itemnumber'},
725 itemtype => $item->{'itype'},
726 borrowernumber => $borrower->{'borrowernumber'},
727 ccode => $item->{'ccode'}}
729 ModDateLastSeen( $item->{'itemnumber'} );
730 return( { STATS => 1 }, {});
733 my $flags = C4::Members::patronflags( $borrower );
734 if ( ref $flags ) {
735 if ( $flags->{GNA} ) {
736 $issuingimpossible{GNA} = 1;
738 if ( $flags->{'LOST'} ) {
739 $issuingimpossible{CARD_LOST} = 1;
741 if ( $flags->{'DBARRED'} ) {
742 $issuingimpossible{DEBARRED} = 1;
745 if ( !defined $borrower->{dateexpiry} || $borrower->{'dateexpiry'} eq '0000-00-00') {
746 $issuingimpossible{EXPIRED} = 1;
747 } else {
748 my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'sql', 'floating' );
749 $expiry_dt->truncate( to => 'day');
750 my $today = $now->clone()->truncate(to => 'day');
751 $today->set_time_zone( 'floating' );
752 if ( DateTime->compare($today, $expiry_dt) == 1 ) {
753 $issuingimpossible{EXPIRED} = 1;
758 # BORROWER STATUS
761 # DEBTS
762 my ($balance, $non_issue_charges, $other_charges) =
763 C4::Members::GetMemberAccountBalance( $borrower->{'borrowernumber'} );
765 my $amountlimit = C4::Context->preference("noissuescharge");
766 my $allowfineoverride = C4::Context->preference("AllowFineOverride");
767 my $allfinesneedoverride = C4::Context->preference("AllFinesNeedOverride");
769 # Check the debt of this patrons guarantees
770 my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
771 $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
772 if ( defined $no_issues_charge_guarantees ) {
773 my $p = Koha::Patrons->find( $borrower->{borrowernumber} );
774 my @guarantees = $p->guarantees();
775 my $guarantees_non_issues_charges;
776 foreach my $g ( @guarantees ) {
777 my ( $b, $n, $o ) = C4::Members::GetMemberAccountBalance( $g->id );
778 $guarantees_non_issues_charges += $n;
781 if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && !$allowfineoverride) {
782 $issuingimpossible{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
783 } elsif ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && $allowfineoverride) {
784 $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
785 } elsif ( $allfinesneedoverride && $guarantees_non_issues_charges > 0 && $guarantees_non_issues_charges <= $no_issues_charge_guarantees && !$inprocess ) {
786 $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
790 if ( C4::Context->preference("IssuingInProcess") ) {
791 if ( $non_issue_charges > $amountlimit && !$inprocess && !$allowfineoverride) {
792 $issuingimpossible{DEBT} = sprintf( "%.2f", $non_issue_charges );
793 } elsif ( $non_issue_charges > $amountlimit && !$inprocess && $allowfineoverride) {
794 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
795 } elsif ( $allfinesneedoverride && $non_issue_charges > 0 && $non_issue_charges <= $amountlimit && !$inprocess ) {
796 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
799 else {
800 if ( $non_issue_charges > $amountlimit && $allowfineoverride ) {
801 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
802 } elsif ( $non_issue_charges > $amountlimit && !$allowfineoverride) {
803 $issuingimpossible{DEBT} = sprintf( "%.2f", $non_issue_charges );
804 } elsif ( $non_issue_charges > 0 && $allfinesneedoverride ) {
805 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
809 if ($balance > 0 && $other_charges > 0) {
810 $alerts{OTHER_CHARGES} = sprintf( "%.2f", $other_charges );
813 my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
814 if ( my $debarred_date = $patron->is_debarred ) {
815 # patron has accrued fine days or has a restriction. $count is a date
816 if ($debarred_date eq '9999-12-31') {
817 $issuingimpossible{USERBLOCKEDNOENDDATE} = $debarred_date;
819 else {
820 $issuingimpossible{USERBLOCKEDWITHENDDATE} = $debarred_date;
822 } elsif ( my $num_overdues = $patron->has_overdues ) {
823 ## patron has outstanding overdue loans
824 if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){
825 $issuingimpossible{USERBLOCKEDOVERDUE} = $num_overdues;
827 elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){
828 $needsconfirmation{USERBLOCKEDOVERDUE} = $num_overdues;
832 # JB34 CHECKS IF BORROWERS DON'T HAVE ISSUE TOO MANY BOOKS
834 my $switch_onsite_checkout = (
835 C4::Context->preference('SwitchOnSiteCheckouts')
836 and $issue
837 and $issue->onsite_checkout
838 and $issue->borrowernumber == $borrower->{'borrowernumber'} ? 1 : 0 );
839 my $toomany = TooMany( $borrower, $item->{biblionumber}, $item, { onsite_checkout => $onsite_checkout, switch_onsite_checkout => $switch_onsite_checkout, } );
840 # if TooMany max_allowed returns 0 the user doesn't have permission to check out this book
841 if ( $toomany ) {
842 if ( $toomany->{max_allowed} == 0 ) {
843 $needsconfirmation{PATRON_CANT} = 1;
845 if ( C4::Context->preference("AllowTooManyOverride") ) {
846 $needsconfirmation{TOO_MANY} = $toomany->{reason};
847 $needsconfirmation{current_loan_count} = $toomany->{count};
848 $needsconfirmation{max_loans_allowed} = $toomany->{max_allowed};
849 } else {
850 $issuingimpossible{TOO_MANY} = $toomany->{reason};
851 $issuingimpossible{current_loan_count} = $toomany->{count};
852 $issuingimpossible{max_loans_allowed} = $toomany->{max_allowed};
857 # CHECKPREVCHECKOUT: CHECK IF ITEM HAS EVER BEEN LENT TO PATRON
859 $patron = Koha::Patrons->find($borrower->{borrowernumber});
860 my $wants_check = $patron->wants_check_for_previous_checkout;
861 $needsconfirmation{PREVISSUE} = 1
862 if ($wants_check and $patron->do_check_for_previous_checkout($item));
865 # ITEM CHECKING
867 if ( $item->{'notforloan'} )
869 if(!C4::Context->preference("AllowNotForLoanOverride")){
870 $issuingimpossible{NOT_FOR_LOAN} = 1;
871 $issuingimpossible{item_notforloan} = $item->{'notforloan'};
872 }else{
873 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
874 $needsconfirmation{item_notforloan} = $item->{'notforloan'};
877 else {
878 # we have to check itemtypes.notforloan also
879 if (C4::Context->preference('item-level_itypes')){
880 # this should probably be a subroutine
881 my $sth = $dbh->prepare("SELECT notforloan FROM itemtypes WHERE itemtype = ?");
882 $sth->execute($item->{'itemtype'});
883 my $notforloan=$sth->fetchrow_hashref();
884 if ($notforloan->{'notforloan'}) {
885 if (!C4::Context->preference("AllowNotForLoanOverride")) {
886 $issuingimpossible{NOT_FOR_LOAN} = 1;
887 $issuingimpossible{itemtype_notforloan} = $item->{'itype'};
888 } else {
889 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
890 $needsconfirmation{itemtype_notforloan} = $item->{'itype'};
894 elsif ($biblioitem->{'notforloan'} == 1){
895 if (!C4::Context->preference("AllowNotForLoanOverride")) {
896 $issuingimpossible{NOT_FOR_LOAN} = 1;
897 $issuingimpossible{itemtype_notforloan} = $biblioitem->{'itemtype'};
898 } else {
899 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
900 $needsconfirmation{itemtype_notforloan} = $biblioitem->{'itemtype'};
904 if ( $item->{'withdrawn'} && $item->{'withdrawn'} > 0 )
906 $issuingimpossible{WTHDRAWN} = 1;
908 if ( $item->{'restricted'}
909 && $item->{'restricted'} == 1 )
911 $issuingimpossible{RESTRICTED} = 1;
913 if ( $item->{'itemlost'} && C4::Context->preference("IssueLostItem") ne 'nothing' ) {
914 my $av = Koha::AuthorisedValues->search({ category => 'LOST', authorised_value => $item->{itemlost} });
915 my $code = $av->count ? $av->next->lib : '';
916 $needsconfirmation{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'confirm' );
917 $alerts{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'alert' );
919 if ( C4::Context->preference("IndependentBranches") ) {
920 my $userenv = C4::Context->userenv;
921 unless ( C4::Context->IsSuperLibrarian() ) {
922 if ( $item->{C4::Context->preference("HomeOrHoldingBranch")} ne $userenv->{branch} ){
923 $issuingimpossible{ITEMNOTSAMEBRANCH} = 1;
924 $issuingimpossible{'itemhomebranch'} = $item->{C4::Context->preference("HomeOrHoldingBranch")};
926 $needsconfirmation{BORRNOTSAMEBRANCH} = $borrower->{'branchcode'}
927 if ( $borrower->{'branchcode'} ne $userenv->{branch} );
931 # CHECK IF THERE IS RENTAL CHARGES. RENTAL MUST BE CONFIRMED BY THE BORROWER
933 my $rentalConfirmation = C4::Context->preference("RentalFeesCheckoutConfirmation");
935 if ( $rentalConfirmation ){
936 my ($rentalCharge) = GetIssuingCharges( $item->{'itemnumber'}, $borrower->{'borrowernumber'} );
937 if ( $rentalCharge > 0 ){
938 $needsconfirmation{RENTALCHARGE} = $rentalCharge;
943 # CHECK IF BOOK ALREADY ISSUED TO THIS BORROWER
945 if ( $issue && $issue->borrowernumber eq $borrower->{'borrowernumber'} ){
947 # Already issued to current borrower.
948 # If it is an on-site checkout if it can be switched to a normal checkout
949 # or ask whether the loan should be renewed
951 if ( $issue->onsite_checkout
952 and C4::Context->preference('SwitchOnSiteCheckouts') ) {
953 $messages{ONSITE_CHECKOUT_WILL_BE_SWITCHED} = 1;
954 } else {
955 my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed(
956 $borrower->{'borrowernumber'},
957 $item->{'itemnumber'},
959 if ( $CanBookBeRenewed == 0 ) { # no more renewals allowed
960 if ( $renewerror eq 'onsite_checkout' ) {
961 $issuingimpossible{NO_RENEWAL_FOR_ONSITE_CHECKOUTS} = 1;
963 else {
964 $issuingimpossible{NO_MORE_RENEWALS} = 1;
967 else {
968 $needsconfirmation{RENEW_ISSUE} = 1;
972 elsif ( $issue ) {
974 # issued to someone else
976 my $patron = Koha::Patrons->find( $issue->borrowernumber );
978 my ( $can_be_returned, $message ) = CanBookBeReturned( $item, C4::Context->userenv->{branch} );
980 unless ( $can_be_returned ) {
981 $issuingimpossible{RETURN_IMPOSSIBLE} = 1;
982 $issuingimpossible{branch_to_return} = $message;
983 } else {
984 $needsconfirmation{ISSUED_TO_ANOTHER} = 1;
985 $needsconfirmation{issued_firstname} = $patron->firstname;
986 $needsconfirmation{issued_surname} = $patron->surname;
987 $needsconfirmation{issued_cardnumber} = $patron->cardnumber;
988 $needsconfirmation{issued_borrowernumber} = $patron->borrowernumber;
992 unless ( $ignore_reserves ) {
993 # See if the item is on reserve.
994 my ( $restype, $res ) = C4::Reserves::CheckReserves( $item->{'itemnumber'} );
995 if ($restype) {
996 my $resbor = $res->{'borrowernumber'};
997 if ( $resbor ne $borrower->{'borrowernumber'} ) {
998 my $patron = Koha::Patrons->find( $resbor );
999 if ( $restype eq "Waiting" )
1001 # The item is on reserve and waiting, but has been
1002 # reserved by some other patron.
1003 $needsconfirmation{RESERVE_WAITING} = 1;
1004 $needsconfirmation{'resfirstname'} = $patron->firstname;
1005 $needsconfirmation{'ressurname'} = $patron->surname;
1006 $needsconfirmation{'rescardnumber'} = $patron->cardnumber;
1007 $needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
1008 $needsconfirmation{'resbranchcode'} = $res->{branchcode};
1009 $needsconfirmation{'reswaitingdate'} = $res->{'waitingdate'};
1011 elsif ( $restype eq "Reserved" ) {
1012 # The item is on reserve for someone else.
1013 $needsconfirmation{RESERVED} = 1;
1014 $needsconfirmation{'resfirstname'} = $patron->firstname;
1015 $needsconfirmation{'ressurname'} = $patron->surname;
1016 $needsconfirmation{'rescardnumber'} = $patron->cardnumber;
1017 $needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
1018 $needsconfirmation{'resbranchcode'} = $patron->branchcode;
1019 $needsconfirmation{'resreservedate'} = $res->{reservedate};
1025 ## CHECK AGE RESTRICTION
1026 my $agerestriction = $biblioitem->{'agerestriction'};
1027 my ($restriction_age, $daysToAgeRestriction) = GetAgeRestriction( $agerestriction, $borrower );
1028 if ( $daysToAgeRestriction && $daysToAgeRestriction > 0 ) {
1029 if ( C4::Context->preference('AgeRestrictionOverride') ) {
1030 $needsconfirmation{AGE_RESTRICTION} = "$agerestriction";
1032 else {
1033 $issuingimpossible{AGE_RESTRICTION} = "$agerestriction";
1037 ## check for high holds decreasing loan period
1038 if ( C4::Context->preference('decreaseLoanHighHolds') ) {
1039 my $check = checkHighHolds( $item, $borrower );
1041 if ( $check->{exceeded} ) {
1042 if ($override_high_holds) {
1043 $alerts{HIGHHOLDS} = {
1044 num_holds => $check->{outstanding},
1045 duration => $check->{duration},
1046 returndate => output_pref( $check->{due_date} ),
1049 else {
1050 $needsconfirmation{HIGHHOLDS} = {
1051 num_holds => $check->{outstanding},
1052 duration => $check->{duration},
1053 returndate => output_pref( $check->{due_date} ),
1059 if (
1060 !C4::Context->preference('AllowMultipleIssuesOnABiblio') &&
1061 # don't do the multiple loans per bib check if we've
1062 # already determined that we've got a loan on the same item
1063 !$issuingimpossible{NO_MORE_RENEWALS} &&
1064 !$needsconfirmation{RENEW_ISSUE}
1066 # Check if borrower has already issued an item from the same biblio
1067 # Only if it's not a subscription
1068 my $biblionumber = $item->{biblionumber};
1069 require C4::Serials;
1070 my $is_a_subscription = C4::Serials::CountSubscriptionFromBiblionumber($biblionumber);
1071 unless ($is_a_subscription) {
1072 my $checkouts = Koha::Checkouts->search(
1074 borrowernumber => $borrower->{borrowernumber},
1075 biblionumber => $biblionumber,
1078 join => 'item',
1081 # if we get here, we don't already have a loan on this item,
1082 # so if there are any loans on this bib, ask for confirmation
1083 if ( $checkouts->count ) {
1084 $needsconfirmation{BIBLIO_ALREADY_ISSUED} = 1;
1089 return ( \%issuingimpossible, \%needsconfirmation, \%alerts, \%messages, );
1092 =head2 CanBookBeReturned
1094 ($returnallowed, $message) = CanBookBeReturned($item, $branch)
1096 Check whether the item can be returned to the provided branch
1098 =over 4
1100 =item C<$item> is a hash of item information as returned from GetItem
1102 =item C<$branch> is the branchcode where the return is taking place
1104 =back
1106 Returns:
1108 =over 4
1110 =item C<$returnallowed> is 0 or 1, corresponding to whether the return is allowed (1) or not (0)
1112 =item C<$message> is the branchcode where the item SHOULD be returned, if the return is not allowed
1114 =back
1116 =cut
1118 sub CanBookBeReturned {
1119 my ($item, $branch) = @_;
1120 my $allowreturntobranch = C4::Context->preference("AllowReturnToBranch") || 'anywhere';
1122 # assume return is allowed to start
1123 my $allowed = 1;
1124 my $message;
1126 # identify all cases where return is forbidden
1127 if ($allowreturntobranch eq 'homebranch' && $branch ne $item->{'homebranch'}) {
1128 $allowed = 0;
1129 $message = $item->{'homebranch'};
1130 } elsif ($allowreturntobranch eq 'holdingbranch' && $branch ne $item->{'holdingbranch'}) {
1131 $allowed = 0;
1132 $message = $item->{'holdingbranch'};
1133 } elsif ($allowreturntobranch eq 'homeorholdingbranch' && $branch ne $item->{'homebranch'} && $branch ne $item->{'holdingbranch'}) {
1134 $allowed = 0;
1135 $message = $item->{'homebranch'}; # FIXME: choice of homebranch is arbitrary
1138 return ($allowed, $message);
1141 =head2 CheckHighHolds
1143 used when syspref decreaseLoanHighHolds is active. Returns 1 or 0 to define whether the minimum value held in
1144 decreaseLoanHighHoldsValue is exceeded, the total number of outstanding holds, the number of days the loan
1145 has been decreased to (held in syspref decreaseLoanHighHoldsValue), and the new due date
1147 =cut
1149 sub checkHighHolds {
1150 my ( $item, $borrower ) = @_;
1151 my $branch = _GetCircControlBranch( $item, $borrower );
1152 my $item_object = Koha::Items->find( $item->{itemnumber} );
1154 my $return_data = {
1155 exceeded => 0,
1156 outstanding => 0,
1157 duration => 0,
1158 due_date => undef,
1161 my $holds = Koha::Holds->search( { biblionumber => $item->{'biblionumber'} } );
1163 if ( $holds->count() ) {
1164 $return_data->{outstanding} = $holds->count();
1166 my $decreaseLoanHighHoldsControl = C4::Context->preference('decreaseLoanHighHoldsControl');
1167 my $decreaseLoanHighHoldsValue = C4::Context->preference('decreaseLoanHighHoldsValue');
1168 my $decreaseLoanHighHoldsIgnoreStatuses = C4::Context->preference('decreaseLoanHighHoldsIgnoreStatuses');
1170 my @decreaseLoanHighHoldsIgnoreStatuses = split( /,/, $decreaseLoanHighHoldsIgnoreStatuses );
1172 if ( $decreaseLoanHighHoldsControl eq 'static' ) {
1174 # static means just more than a given number of holds on the record
1176 # If the number of holds is less than the threshold, we can stop here
1177 if ( $holds->count() < $decreaseLoanHighHoldsValue ) {
1178 return $return_data;
1181 elsif ( $decreaseLoanHighHoldsControl eq 'dynamic' ) {
1183 # dynamic means X more than the number of holdable items on the record
1185 # let's get the items
1186 my @items = $holds->next()->biblio()->items();
1188 # Remove any items with status defined to be ignored even if the would not make item unholdable
1189 foreach my $status (@decreaseLoanHighHoldsIgnoreStatuses) {
1190 @items = grep { !$_->$status } @items;
1193 # Remove any items that are not holdable for this patron
1194 @items = grep { CanItemBeReserved( $borrower->{borrowernumber}, $_->itemnumber ) eq 'OK' } @items;
1196 my $items_count = scalar @items;
1198 my $threshold = $items_count + $decreaseLoanHighHoldsValue;
1200 # If the number of holds is less than the count of items we have
1201 # plus the number of holds allowed above that count, we can stop here
1202 if ( $holds->count() <= $threshold ) {
1203 return $return_data;
1207 my $issuedate = DateTime->now( time_zone => C4::Context->tz() );
1209 my $calendar = Koha::Calendar->new( branchcode => $branch );
1211 my $itype = $item_object->effective_itemtype;
1212 my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branch, $borrower );
1214 my $decreaseLoanHighHoldsDuration = C4::Context->preference('decreaseLoanHighHoldsDuration');
1216 my $reduced_datedue = $calendar->addDate( $issuedate, $decreaseLoanHighHoldsDuration );
1217 $reduced_datedue->set_hour($orig_due->hour);
1218 $reduced_datedue->set_minute($orig_due->minute);
1219 $reduced_datedue->truncate( to => 'minute' );
1221 if ( DateTime->compare( $reduced_datedue, $orig_due ) == -1 ) {
1222 $return_data->{exceeded} = 1;
1223 $return_data->{duration} = $decreaseLoanHighHoldsDuration;
1224 $return_data->{due_date} = $reduced_datedue;
1228 return $return_data;
1231 =head2 AddIssue
1233 &AddIssue($borrower, $barcode, [$datedue], [$cancelreserve], [$issuedate])
1235 Issue a book. Does no check, they are done in CanBookBeIssued. If we reach this sub, it means the user confirmed if needed.
1237 =over 4
1239 =item C<$borrower> is a hash with borrower informations (from Koha::Patron->unblessed).
1241 =item C<$barcode> is the barcode of the item being issued.
1243 =item C<$datedue> is a DateTime object for the max date of return, i.e. the date due (optional).
1244 Calculated if empty.
1246 =item C<$cancelreserve> is 1 to override and cancel any pending reserves for the item (optional).
1248 =item C<$issuedate> is the date to issue the item in iso (YYYY-MM-DD) format (optional).
1249 Defaults to today. Unlike C<$datedue>, NOT a DateTime object, unfortunately.
1251 AddIssue does the following things :
1253 - step 01: check that there is a borrowernumber & a barcode provided
1254 - check for RENEWAL (book issued & being issued to the same patron)
1255 - renewal YES = Calculate Charge & renew
1256 - renewal NO =
1257 * BOOK ACTUALLY ISSUED ? do a return if book is actually issued (but to someone else)
1258 * RESERVE PLACED ?
1259 - fill reserve if reserve to this patron
1260 - cancel reserve or not, otherwise
1261 * TRANSFERT PENDING ?
1262 - complete the transfert
1263 * ISSUE THE BOOK
1265 =back
1267 =cut
1269 sub AddIssue {
1270 my ( $borrower, $barcode, $datedue, $cancelreserve, $issuedate, $sipmode, $params ) = @_;
1272 my $onsite_checkout = $params && $params->{onsite_checkout} ? 1 : 0;
1273 my $switch_onsite_checkout = $params && $params->{switch_onsite_checkout};
1274 my $auto_renew = $params && $params->{auto_renew};
1275 my $dbh = C4::Context->dbh;
1276 my $barcodecheck = CheckValidBarcode($barcode);
1278 my $issue;
1280 if ( $datedue && ref $datedue ne 'DateTime' ) {
1281 $datedue = dt_from_string($datedue);
1284 # $issuedate defaults to today.
1285 if ( !defined $issuedate ) {
1286 $issuedate = DateTime->now( time_zone => C4::Context->tz() );
1288 else {
1289 if ( ref $issuedate ne 'DateTime' ) {
1290 $issuedate = dt_from_string($issuedate);
1295 # Stop here if the patron or barcode doesn't exist
1296 if ( $borrower && $barcode && $barcodecheck ) {
1297 # find which item we issue
1298 my $item = GetItem( '', $barcode )
1299 or return; # if we don't get an Item, abort.
1300 my $item_object = Koha::Items->find( { barcode => $barcode } );
1302 my $branch = _GetCircControlBranch( $item, $borrower );
1304 # get actual issuing if there is one
1305 my $actualissue = Koha::Checkouts->find( { itemnumber => $item->{itemnumber} } );
1307 # check if we just renew the issue.
1308 if ( $actualissue and $actualissue->borrowernumber eq $borrower->{'borrowernumber'}
1309 and not $switch_onsite_checkout ) {
1310 $datedue = AddRenewal(
1311 $borrower->{'borrowernumber'},
1312 $item->{'itemnumber'},
1313 $branch,
1314 $datedue,
1315 $issuedate, # here interpreted as the renewal date
1318 else {
1319 # it's NOT a renewal
1320 if ( $actualissue and not $switch_onsite_checkout ) {
1321 # This book is currently on loan, but not to the person
1322 # who wants to borrow it now. mark it returned before issuing to the new borrower
1323 my ( $allowed, $message ) = CanBookBeReturned( $item, C4::Context->userenv->{branch} );
1324 return unless $allowed;
1325 AddReturn( $item->{'barcode'}, C4::Context->userenv->{'branch'} );
1328 MoveReserve( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $cancelreserve );
1330 # Starting process for transfer job (checking transfert and validate it if we have one)
1331 my ($datesent) = GetTransfers( $item->{'itemnumber'} );
1332 if ($datesent) {
1333 # updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....)
1334 my $sth = $dbh->prepare(
1335 "UPDATE branchtransfers
1336 SET datearrived = now(),
1337 tobranch = ?,
1338 comments = 'Forced branchtransfer'
1339 WHERE itemnumber= ? AND datearrived IS NULL"
1341 $sth->execute( C4::Context->userenv->{'branch'},
1342 $item->{'itemnumber'} );
1345 # If automatic renewal wasn't selected while issuing, set the value according to the issuing rule.
1346 unless ($auto_renew) {
1347 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
1348 { categorycode => $borrower->{categorycode},
1349 itemtype => $item->{itype},
1350 branchcode => $branch
1354 $auto_renew = $issuing_rule->auto_renew if $issuing_rule;
1357 # Record in the database the fact that the book was issued.
1358 unless ($datedue) {
1359 my $itype = $item_object->effective_itemtype;
1360 $datedue = CalcDateDue( $issuedate, $itype, $branch, $borrower );
1363 $datedue->truncate( to => 'minute' );
1365 $issue = Koha::Database->new()->schema()->resultset('Issue')->update_or_create(
1367 borrowernumber => $borrower->{'borrowernumber'},
1368 itemnumber => $item->{'itemnumber'},
1369 issuedate => $issuedate->strftime('%Y-%m-%d %H:%M:%S'),
1370 date_due => $datedue->strftime('%Y-%m-%d %H:%M:%S'),
1371 branchcode => C4::Context->userenv->{'branch'},
1372 onsite_checkout => $onsite_checkout,
1373 auto_renew => $auto_renew ? 1 : 0
1377 if ( C4::Context->preference('ReturnToShelvingCart') ) {
1378 # ReturnToShelvingCart is on, anything issued should be taken off the cart.
1379 CartToShelf( $item->{'itemnumber'} );
1381 $item->{'issues'}++;
1382 if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) {
1383 UpdateTotalIssues( $item->{'biblionumber'}, 1 );
1386 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1387 if ( $item->{'itemlost'} ) {
1388 if (
1389 Koha::RefundLostItemFeeRules->should_refund(
1391 current_branch => C4::Context->userenv->{branch},
1392 item_home_branch => $item->{homebranch},
1393 item_holding_branch => $item->{holdingbranch}
1398 _FixAccountForLostAndReturned( $item->{'itemnumber'}, undef,
1399 $item->{'barcode'} );
1403 ModItem(
1405 issues => $item->{'issues'},
1406 holdingbranch => C4::Context->userenv->{'branch'},
1407 itemlost => 0,
1408 onloan => $datedue->ymd(),
1409 datelastborrowed => DateTime->now( time_zone => C4::Context->tz() )->ymd(),
1411 $item->{'biblionumber'},
1412 $item->{'itemnumber'}
1414 ModDateLastSeen( $item->{'itemnumber'} );
1416 # If it costs to borrow this book, charge it to the patron's account.
1417 my ( $charge, $itemtype ) = GetIssuingCharges( $item->{'itemnumber'}, $borrower->{'borrowernumber'} );
1418 if ( $charge > 0 ) {
1419 AddIssuingCharge( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $charge );
1420 $item->{'charge'} = $charge;
1423 # Record the fact that this book was issued.
1424 &UpdateStats(
1426 branch => C4::Context->userenv->{'branch'},
1427 type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
1428 amount => $charge,
1429 other => ( $sipmode ? "SIP-$sipmode" : '' ),
1430 itemnumber => $item->{'itemnumber'},
1431 itemtype => $item->{'itype'},
1432 borrowernumber => $borrower->{'borrowernumber'},
1433 ccode => $item->{'ccode'}
1437 # Send a checkout slip.
1438 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1439 my %conditions = (
1440 branchcode => $branch,
1441 categorycode => $borrower->{categorycode},
1442 item_type => $item->{itype},
1443 notification => 'CHECKOUT',
1445 if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
1446 SendCirculationAlert(
1448 type => 'CHECKOUT',
1449 item => $item,
1450 borrower => $borrower,
1451 branch => $branch,
1455 logaction(
1456 "CIRCULATION", "ISSUE",
1457 $borrower->{'borrowernumber'},
1458 $item->{'itemnumber'}
1459 ) if C4::Context->preference("IssueLog");
1462 return $issue;
1465 =head2 GetLoanLength
1467 my $loanlength = &GetLoanLength($borrowertype,$itemtype,branchcode)
1469 Get loan length for an itemtype, a borrower type and a branch
1471 =cut
1473 sub GetLoanLength {
1474 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1475 my $dbh = C4::Context->dbh;
1476 my $sth = $dbh->prepare(qq{
1477 SELECT issuelength, lengthunit, renewalperiod
1478 FROM issuingrules
1479 WHERE categorycode=?
1480 AND itemtype=?
1481 AND branchcode=?
1482 AND issuelength IS NOT NULL
1485 # try to find issuelength & return the 1st available.
1486 # check with borrowertype, itemtype and branchcode, then without one of those parameters
1487 $sth->execute( $borrowertype, $itemtype, $branchcode );
1488 my $loanlength = $sth->fetchrow_hashref;
1490 return $loanlength
1491 if defined($loanlength) && defined $loanlength->{issuelength};
1493 $sth->execute( $borrowertype, '*', $branchcode );
1494 $loanlength = $sth->fetchrow_hashref;
1495 return $loanlength
1496 if defined($loanlength) && defined $loanlength->{issuelength};
1498 $sth->execute( '*', $itemtype, $branchcode );
1499 $loanlength = $sth->fetchrow_hashref;
1500 return $loanlength
1501 if defined($loanlength) && defined $loanlength->{issuelength};
1503 $sth->execute( '*', '*', $branchcode );
1504 $loanlength = $sth->fetchrow_hashref;
1505 return $loanlength
1506 if defined($loanlength) && defined $loanlength->{issuelength};
1508 $sth->execute( $borrowertype, $itemtype, '*' );
1509 $loanlength = $sth->fetchrow_hashref;
1510 return $loanlength
1511 if defined($loanlength) && defined $loanlength->{issuelength};
1513 $sth->execute( $borrowertype, '*', '*' );
1514 $loanlength = $sth->fetchrow_hashref;
1515 return $loanlength
1516 if defined($loanlength) && defined $loanlength->{issuelength};
1518 $sth->execute( '*', $itemtype, '*' );
1519 $loanlength = $sth->fetchrow_hashref;
1520 return $loanlength
1521 if defined($loanlength) && defined $loanlength->{issuelength};
1523 $sth->execute( '*', '*', '*' );
1524 $loanlength = $sth->fetchrow_hashref;
1525 return $loanlength
1526 if defined($loanlength) && defined $loanlength->{issuelength};
1528 # if no rule is set => 0 day (hardcoded)
1529 return {
1530 issuelength => 0,
1531 renewalperiod => 0,
1532 lengthunit => 'days',
1538 =head2 GetHardDueDate
1540 my ($hardduedate,$hardduedatecompare) = &GetHardDueDate($borrowertype,$itemtype,branchcode)
1542 Get the Hard Due Date and it's comparison for an itemtype, a borrower type and a branch
1544 =cut
1546 sub GetHardDueDate {
1547 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1549 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
1550 { categorycode => $borrowertype,
1551 itemtype => $itemtype,
1552 branchcode => $branchcode
1557 if ( defined( $issuing_rule ) ) {
1558 if ( $issuing_rule->hardduedate ) {
1559 return (dt_from_string($issuing_rule->hardduedate, 'iso'),$issuing_rule->hardduedatecompare);
1560 } else {
1561 return (undef, undef);
1566 =head2 GetBranchBorrowerCircRule
1568 my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
1570 Retrieves circulation rule attributes that apply to the given
1571 branch and patron category, regardless of item type.
1572 The return value is a hashref containing the following key:
1574 maxissueqty - maximum number of loans that a
1575 patron of the given category can have at the given
1576 branch. If the value is undef, no limit.
1578 maxonsiteissueqty - maximum of on-site checkouts that a
1579 patron of the given category can have at the given
1580 branch. If the value is undef, no limit.
1582 This will first check for a specific branch and
1583 category match from branch_borrower_circ_rules.
1585 If no rule is found, it will then check default_branch_circ_rules
1586 (same branch, default category). If no rule is found,
1587 it will then check default_borrower_circ_rules (default
1588 branch, same category), then failing that, default_circ_rules
1589 (default branch, default category).
1591 If no rule has been found in the database, it will default to
1592 the buillt in rule:
1594 maxissueqty - undef
1595 maxonsiteissueqty - undef
1597 C<$branchcode> and C<$categorycode> should contain the
1598 literal branch code and patron category code, respectively - no
1599 wildcards.
1601 =cut
1603 sub GetBranchBorrowerCircRule {
1604 my ( $branchcode, $categorycode ) = @_;
1606 my $rules;
1607 my $dbh = C4::Context->dbh();
1608 $rules = $dbh->selectrow_hashref( q|
1609 SELECT maxissueqty, maxonsiteissueqty
1610 FROM branch_borrower_circ_rules
1611 WHERE branchcode = ?
1612 AND categorycode = ?
1613 |, {}, $branchcode, $categorycode ) ;
1614 return $rules if $rules;
1616 # try same branch, default borrower category
1617 $rules = $dbh->selectrow_hashref( q|
1618 SELECT maxissueqty, maxonsiteissueqty
1619 FROM default_branch_circ_rules
1620 WHERE branchcode = ?
1621 |, {}, $branchcode ) ;
1622 return $rules if $rules;
1624 # try default branch, same borrower category
1625 $rules = $dbh->selectrow_hashref( q|
1626 SELECT maxissueqty, maxonsiteissueqty
1627 FROM default_borrower_circ_rules
1628 WHERE categorycode = ?
1629 |, {}, $categorycode ) ;
1630 return $rules if $rules;
1632 # try default branch, default borrower category
1633 $rules = $dbh->selectrow_hashref( q|
1634 SELECT maxissueqty, maxonsiteissueqty
1635 FROM default_circ_rules
1636 |, {} );
1637 return $rules if $rules;
1639 # built-in default circulation rule
1640 return {
1641 maxissueqty => undef,
1642 maxonsiteissueqty => undef,
1646 =head2 GetBranchItemRule
1648 my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
1650 Retrieves circulation rule attributes that apply to the given
1651 branch and item type, regardless of patron category.
1653 The return value is a hashref containing the following keys:
1655 holdallowed => Hold policy for this branch and itemtype. Possible values:
1656 0: No holds allowed.
1657 1: Holds allowed only by patrons that have the same homebranch as the item.
1658 2: Holds allowed from any patron.
1660 returnbranch => branch to which to return item. Possible values:
1661 noreturn: do not return, let item remain where checked in (floating collections)
1662 homebranch: return to item's home branch
1663 holdingbranch: return to issuer branch
1665 This searches branchitemrules in the following order:
1667 * Same branchcode and itemtype
1668 * Same branchcode, itemtype '*'
1669 * branchcode '*', same itemtype
1670 * branchcode and itemtype '*'
1672 Neither C<$branchcode> nor C<$itemtype> should be '*'.
1674 =cut
1676 sub GetBranchItemRule {
1677 my ( $branchcode, $itemtype ) = @_;
1678 my $dbh = C4::Context->dbh();
1679 my $result = {};
1681 my @attempts = (
1682 ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1683 FROM branch_item_rules
1684 WHERE branchcode = ?
1685 AND itemtype = ?', $branchcode, $itemtype],
1686 ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1687 FROM default_branch_circ_rules
1688 WHERE branchcode = ?', $branchcode],
1689 ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1690 FROM default_branch_item_rules
1691 WHERE itemtype = ?', $itemtype],
1692 ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1693 FROM default_circ_rules'],
1696 foreach my $attempt (@attempts) {
1697 my ($query, @bind_params) = @{$attempt};
1698 my $search_result = $dbh->selectrow_hashref ( $query , {}, @bind_params )
1699 or next;
1701 # Since branch/category and branch/itemtype use the same per-branch
1702 # defaults tables, we have to check that the key we want is set, not
1703 # just that a row was returned
1704 $result->{'holdallowed'} = $search_result->{'holdallowed'} unless ( defined $result->{'holdallowed'} );
1705 $result->{'hold_fulfillment_policy'} = $search_result->{'hold_fulfillment_policy'} unless ( defined $result->{'hold_fulfillment_policy'} );
1706 $result->{'returnbranch'} = $search_result->{'returnbranch'} unless ( defined $result->{'returnbranch'} );
1709 # built-in default circulation rule
1710 $result->{'holdallowed'} = 2 unless ( defined $result->{'holdallowed'} );
1711 $result->{'hold_fulfillment_policy'} = 'any' unless ( defined $result->{'hold_fulfillment_policy'} );
1712 $result->{'returnbranch'} = 'homebranch' unless ( defined $result->{'returnbranch'} );
1714 return $result;
1717 =head2 AddReturn
1719 ($doreturn, $messages, $iteminformation, $borrower) =
1720 &AddReturn( $barcode, $branch [,$exemptfine] [,$dropbox] [,$returndate] );
1722 Returns a book.
1724 =over 4
1726 =item C<$barcode> is the bar code of the book being returned.
1728 =item C<$branch> is the code of the branch where the book is being returned.
1730 =item C<$exemptfine> indicates that overdue charges for the item will be
1731 removed. Optional.
1733 =item C<$dropbox> indicates that the check-in date is assumed to be
1734 yesterday, or the last non-holiday as defined in C4::Calendar . If
1735 overdue charges are applied and C<$dropbox> is true, the last charge
1736 will be removed. This assumes that the fines accrual script has run
1737 for _today_. Optional.
1739 =item C<$return_date> allows the default return date to be overridden
1740 by the given return date. Optional.
1742 =back
1744 C<&AddReturn> returns a list of four items:
1746 C<$doreturn> is true iff the return succeeded.
1748 C<$messages> is a reference-to-hash giving feedback on the operation.
1749 The keys of the hash are:
1751 =over 4
1753 =item C<BadBarcode>
1755 No item with this barcode exists. The value is C<$barcode>.
1757 =item C<NotIssued>
1759 The book is not currently on loan. The value is C<$barcode>.
1761 =item C<IsPermanent>
1763 The book's home branch is a permanent collection. If you have borrowed
1764 this book, you are not allowed to return it. The value is the code for
1765 the book's home branch.
1767 =item C<withdrawn>
1769 This book has been withdrawn/cancelled. The value should be ignored.
1771 =item C<Wrongbranch>
1773 This book has was returned to the wrong branch. The value is a hashref
1774 so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
1775 contain the branchcode of the incorrect and correct return library, respectively.
1777 =item C<ResFound>
1779 The item was reserved. The value is a reference-to-hash whose keys are
1780 fields from the reserves table of the Koha database, and
1781 C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
1782 either C<Waiting>, C<Reserved>, or 0.
1784 =item C<WasReturned>
1786 Value 1 if return is successful.
1788 =item C<NeedsTransfer>
1790 If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer.
1792 =back
1794 C<$iteminformation> is a reference-to-hash, giving information about the
1795 returned item from the issues table.
1797 C<$borrower> is a reference-to-hash, giving information about the
1798 patron who last borrowed the book.
1800 =cut
1802 sub AddReturn {
1803 my ( $barcode, $branch, $exemptfine, $dropbox, $return_date, $dropboxdate ) = @_;
1805 if ($branch and not Koha::Libraries->find($branch)) {
1806 warn "AddReturn error: branch '$branch' not found. Reverting to " . C4::Context->userenv->{'branch'};
1807 undef $branch;
1809 $branch = C4::Context->userenv->{'branch'} unless $branch; # we trust userenv to be a safe fallback/default
1810 my $messages;
1811 my $patron;
1812 my $doreturn = 1;
1813 my $validTransfert = 0;
1814 my $stat_type = 'return';
1816 # get information on item
1817 my $item = GetItem( undef, $barcode );
1818 unless ($item) {
1819 return ( 0, { BadBarcode => $barcode } ); # no barcode means no item or borrower. bail out.
1822 my $itemnumber = $item->{ itemnumber };
1823 my $itemtype = $item->{itype}; # GetItem called effective_itemtype
1825 my $issue = Koha::Checkouts->find( { itemnumber => $itemnumber } );
1826 if ( $issue ) {
1827 $patron = Koha::Patrons->find( $issue->borrowernumber )
1828 or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existent borrowernumber '" . $issue->borrowernumber . "'\n"
1829 . Dumper($issue->unblessed) . "\n";
1830 } else {
1831 $messages->{'NotIssued'} = $barcode;
1832 # even though item is not on loan, it may still be transferred; therefore, get current branch info
1833 $doreturn = 0;
1834 # No issue, no borrowernumber. ONLY if $doreturn, *might* you have a $borrower later.
1835 # Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
1836 if (C4::Context->preference("RecordLocalUseOnReturn")) {
1837 $messages->{'LocalUse'} = 1;
1838 $stat_type = 'localuse';
1842 if ( $item->{'location'} eq 'PROC' ) {
1843 if ( C4::Context->preference("InProcessingToShelvingCart") ) {
1844 $item->{'location'} = 'CART';
1846 else {
1847 $item->{location} = $item->{permanent_location};
1850 ModItem( $item, $item->{'biblionumber'}, $item->{'itemnumber'} );
1853 # full item data, but no borrowernumber or checkout info (no issue)
1854 my $hbr = GetBranchItemRule($item->{'homebranch'}, $item->{'itype'})->{'returnbranch'} || "homebranch";
1855 # get the proper branch to which to return the item
1856 my $returnbranch = $item->{$hbr} || $branch ;
1857 # if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
1859 my $borrowernumber = $patron ? $patron->borrowernumber : undef; # we don't know if we had a borrower or not
1860 my $patron_unblessed = $patron ? $patron->unblessed : {};
1862 my $yaml = C4::Context->preference('UpdateNotForLoanStatusOnCheckin');
1863 if ($yaml) {
1864 $yaml = "$yaml\n\n"; # YAML is anal on ending \n. Surplus does not hurt
1865 my $rules;
1866 eval { $rules = YAML::Load($yaml); };
1867 if ($@) {
1868 warn "Unable to parse UpdateNotForLoanStatusOnCheckin syspref : $@";
1870 else {
1871 foreach my $key ( keys %$rules ) {
1872 if ( $item->{notforloan} eq $key ) {
1873 $messages->{'NotForLoanStatusUpdated'} = { from => $item->{notforloan}, to => $rules->{$key} };
1874 ModItem( { notforloan => $rules->{$key} }, undef, $itemnumber );
1875 last;
1882 # check if the book is in a permanent collection....
1883 # FIXME -- This 'PE' attribute is largely undocumented. afaict, there's no user interface that reflects this functionality.
1884 if ( $returnbranch ) {
1885 my $library = Koha::Libraries->find($returnbranch);
1886 if ( $library and $library->get_categories->search({'me.categorycode' => 'PE'})->count ) {
1887 $messages->{'IsPermanent'} = $returnbranch;
1891 # check if the return is allowed at this branch
1892 my ($returnallowed, $message) = CanBookBeReturned($item, $branch);
1893 unless ($returnallowed){
1894 $messages->{'Wrongbranch'} = {
1895 Wrongbranch => $branch,
1896 Rightbranch => $message
1898 $doreturn = 0;
1899 return ( $doreturn, $messages, $issue, $patron_unblessed);
1902 if ( $item->{'withdrawn'} ) { # book has been cancelled
1903 $messages->{'withdrawn'} = 1;
1904 $doreturn = 0 if C4::Context->preference("BlockReturnOfWithdrawnItems");
1907 # case of a return of document (deal with issues and holdingbranch)
1908 my $today = DateTime->now( time_zone => C4::Context->tz() );
1910 if ($doreturn) {
1911 my $is_overdue;
1912 die "The item is not issed and cannot be returned" unless $issue; # Just in case...
1913 $patron or warn "AddReturn without current borrower";
1914 my $circControlBranch;
1915 if ($dropbox) {
1916 # define circControlBranch only if dropbox mode is set
1917 # don't allow dropbox mode to create an invalid entry in issues (issuedate > today)
1918 # FIXME: check issuedate > returndate, factoring in holidays
1920 $circControlBranch = _GetCircControlBranch($item,$patron_unblessed);
1921 $is_overdue = $issue->is_overdue( $dropboxdate );
1922 } else {
1923 $is_overdue = $issue->is_overdue;
1926 if ($patron) {
1927 eval {
1928 MarkIssueReturned( $borrowernumber, $item->{'itemnumber'},
1929 $circControlBranch, $return_date, $patron->privacy );
1931 unless ( $@ ) {
1932 if ( ( C4::Context->preference('CalculateFinesOnReturn') && $is_overdue ) || $return_date ) {
1933 _CalculateAndUpdateFine( { issue => $issue, item => $item, borrower => $patron_unblessed, return_date => $return_date } );
1935 } else {
1936 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 );
1938 return ( 0, { WasReturned => 0, DataCorrupted => 1 }, $issue, $patron_unblessed );
1941 # FIXME is the "= 1" right? This could be the borrower hash.
1942 $messages->{'WasReturned'} = 1;
1946 ModItem({ onloan => undef }, $item->{biblionumber}, $item->{'itemnumber'});
1949 # the holdingbranch is updated if the document is returned to another location.
1950 # this is always done regardless of whether the item was on loan or not
1951 my $item_holding_branch = $item->{ holdingbranch };
1952 if ($item->{'holdingbranch'} ne $branch) {
1953 UpdateHoldingbranch($branch, $item->{'itemnumber'});
1954 $item->{'holdingbranch'} = $branch; # update item data holdingbranch too
1956 ModDateLastSeen( $item->{'itemnumber'} );
1958 # check if we have a transfer for this document
1959 my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->{'itemnumber'} );
1961 # if we have a transfer to do, we update the line of transfers with the datearrived
1962 my $is_in_rotating_collection = C4::RotatingCollections::isItemInAnyCollection( $item->{'itemnumber'} );
1963 if ($datesent) {
1964 if ( $tobranch eq $branch ) {
1965 my $sth = C4::Context->dbh->prepare(
1966 "UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL"
1968 $sth->execute( $item->{'itemnumber'} );
1969 # if we have a reservation with valid transfer, we can set it's status to 'W'
1970 ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
1971 C4::Reserves::ModReserveStatus($item->{'itemnumber'}, 'W');
1972 } else {
1973 $messages->{'WrongTransfer'} = $tobranch;
1974 $messages->{'WrongTransferItem'} = $item->{'itemnumber'};
1976 $validTransfert = 1;
1977 } else {
1978 ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
1981 # fix up the accounts.....
1982 if ( $item->{'itemlost'} ) {
1983 $messages->{'WasLost'} = 1;
1985 if ( $item->{'itemlost'} ) {
1986 if (
1987 Koha::RefundLostItemFeeRules->should_refund(
1989 current_branch => C4::Context->userenv->{branch},
1990 item_home_branch => $item->{homebranch},
1991 item_holding_branch => $item_holding_branch
1996 _FixAccountForLostAndReturned( $item->{'itemnumber'}, $borrowernumber, $barcode );
1997 $messages->{'LostItemFeeRefunded'} = 1;
2002 # fix up the overdues in accounts...
2003 if ($borrowernumber) {
2004 my $fix = _FixOverduesOnReturn($borrowernumber, $item->{itemnumber}, $exemptfine, $dropbox);
2005 defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $item->{itemnumber}...) failed!"; # zero is OK, check defined
2007 if ( $issue and $issue->is_overdue ) {
2008 # fix fine days
2009 $today = $dropboxdate if $dropbox;
2010 my ($debardate,$reminder) = _debar_user_on_return( $patron_unblessed, $item, dt_from_string($issue->date_due), $today );
2011 if ($reminder){
2012 $messages->{'PrevDebarred'} = $debardate;
2013 } else {
2014 $messages->{'Debarred'} = $debardate if $debardate;
2016 # there's no overdue on the item but borrower had been previously debarred
2017 } elsif ( $issue->date_due and $patron->debarred ) {
2018 if ( $patron->debarred eq "9999-12-31") {
2019 $messages->{'ForeverDebarred'} = $patron->debarred;
2020 } else {
2021 my $borrower_debar_dt = dt_from_string( $patron->debarred );
2022 $borrower_debar_dt->truncate(to => 'day');
2023 my $today_dt = $today->clone()->truncate(to => 'day');
2024 if ( DateTime->compare( $borrower_debar_dt, $today_dt ) != -1 ) {
2025 $messages->{'PrevDebarred'} = $patron->debarred;
2031 # find reserves.....
2032 # if we don't have a reserve with the status W, we launch the Checkreserves routine
2033 my ($resfound, $resrec);
2034 my $lookahead= C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
2035 ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->{'itemnumber'}, undef, $lookahead ) unless ( $item->{'withdrawn'} );
2036 if ($resfound) {
2037 $resrec->{'ResFound'} = $resfound;
2038 $messages->{'ResFound'} = $resrec;
2041 # Record the fact that this book was returned.
2042 UpdateStats({
2043 branch => $branch,
2044 type => $stat_type,
2045 itemnumber => $itemnumber,
2046 itemtype => $itemtype,
2047 borrowernumber => $borrowernumber,
2048 ccode => $item->{ ccode }
2051 # Send a check-in slip. # NOTE: borrower may be undef. Do not try to send messages then.
2052 if ( $patron ) {
2053 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2054 my %conditions = (
2055 branchcode => $branch,
2056 categorycode => $patron->categorycode,
2057 item_type => $item->{itype},
2058 notification => 'CHECKIN',
2060 if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
2061 SendCirculationAlert({
2062 type => 'CHECKIN',
2063 item => $item,
2064 borrower => $patron->unblessed,
2065 branch => $branch,
2069 logaction("CIRCULATION", "RETURN", $borrowernumber, $item->{'itemnumber'})
2070 if C4::Context->preference("ReturnLog");
2073 # Remove any OVERDUES related debarment if the borrower has no overdues
2074 if ( $borrowernumber
2075 && $patron->debarred
2076 && C4::Context->preference('AutoRemoveOverduesRestrictions')
2077 && !Koha::Patrons->find( $borrowernumber )->has_overdues
2078 && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2080 DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2083 # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
2084 if (!$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) and not $messages->{'WrongTransfer'}){
2085 if (C4::Context->preference("AutomaticItemReturn" ) or
2086 (C4::Context->preference("UseBranchTransferLimits") and
2087 ! IsBranchTransferAllowed($branch, $returnbranch, $item->{C4::Context->preference("BranchTransferLimitsType")} )
2088 )) {
2089 $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s)", $item->{'itemnumber'},$branch, $returnbranch;
2090 $debug and warn "item: " . Dumper($item);
2091 ModItemTransfer($item->{'itemnumber'}, $branch, $returnbranch);
2092 $messages->{'WasTransfered'} = 1;
2093 } else {
2094 $messages->{'NeedsTransfer'} = $returnbranch;
2098 return ( $doreturn, $messages, $issue, ( $patron ? $patron->unblessed : {} ));
2101 =head2 MarkIssueReturned
2103 MarkIssueReturned($borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy);
2105 Unconditionally marks an issue as being returned by
2106 moving the C<issues> row to C<old_issues> and
2107 setting C<returndate> to the current date, or
2108 the last non-holiday date of the branccode specified in
2109 C<dropbox_branch> . Assumes you've already checked that
2110 it's safe to do this, i.e. last non-holiday > issuedate.
2112 if C<$returndate> is specified (in iso format), it is used as the date
2113 of the return. It is ignored when a dropbox_branch is passed in.
2115 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
2116 the old_issue is immediately anonymised
2118 Ideally, this function would be internal to C<C4::Circulation>,
2119 not exported, but it is currently needed by one
2120 routine in C<C4::Accounts>.
2122 =cut
2124 sub MarkIssueReturned {
2125 my ( $borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy ) = @_;
2127 my $anonymouspatron;
2128 if ( $privacy == 2 ) {
2129 # The default of 0 will not work due to foreign key constraints
2130 # The anonymisation will fail if AnonymousPatron is not a valid entry
2131 # We need to check if the anonymous patron exist, Koha will fail loudly if it does not
2132 # Note that a warning should appear on the about page (System information tab).
2133 $anonymouspatron = C4::Context->preference('AnonymousPatron');
2134 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."
2135 unless Koha::Patrons->find( $anonymouspatron );
2137 my $database = Koha::Database->new();
2138 my $schema = $database->schema;
2139 my $dbh = C4::Context->dbh;
2141 my $issue_id = $dbh->selectrow_array(
2142 q|SELECT issue_id FROM issues WHERE itemnumber = ?|,
2143 undef, $itemnumber
2146 my $query = 'UPDATE issues SET returndate=';
2147 my @bind;
2148 if ($dropbox_branch) {
2149 my $calendar = Koha::Calendar->new( branchcode => $dropbox_branch );
2150 my $dropboxdate = $calendar->addDate( DateTime->now( time_zone => C4::Context->tz), -1 );
2151 $query .= ' ? ';
2152 push @bind, $dropboxdate->strftime('%Y-%m-%d %H:%M');
2153 } elsif ($returndate) {
2154 $query .= ' ? ';
2155 push @bind, $returndate;
2156 } else {
2157 $query .= ' now() ';
2159 $query .= ' WHERE issue_id = ?';
2160 push @bind, $issue_id;
2162 # FIXME Improve the return value and handle it from callers
2163 $schema->txn_do(sub {
2165 # Update the returndate
2166 $dbh->do( $query, undef, @bind );
2168 # Retrieve the issue
2169 my $issue = Koha::Checkouts->find( $issue_id ); # FIXME should be fetched earlier
2171 # Create the old_issues entry
2172 my $old_checkout = Koha::Old::Checkout->new($issue->unblessed)->store;
2174 # Update the fines
2175 $dbh->do(q|UPDATE accountlines SET issue_id = ? WHERE issue_id = ?|, undef, $old_checkout->issue_id, $issue->issue_id);
2177 # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
2178 if ( $privacy == 2) {
2179 $dbh->do(q|UPDATE old_issues SET borrowernumber=? WHERE issue_id = ?|, undef, $anonymouspatron, $old_checkout->issue_id);
2182 # And finally delete the issue
2183 $issue->delete;
2185 ModItem( { 'onloan' => undef }, undef, $itemnumber );
2187 if ( C4::Context->preference('StoreLastBorrower') ) {
2188 my $item = Koha::Items->find( $itemnumber );
2189 my $patron = Koha::Patrons->find( $borrowernumber );
2190 $item->last_returned_by( $patron );
2194 return $issue_id;
2197 =head2 _debar_user_on_return
2199 _debar_user_on_return($borrower, $item, $datedue, today);
2201 C<$borrower> borrower hashref
2203 C<$item> item hashref
2205 C<$datedue> date due DateTime object
2207 C<$return_date> DateTime object representing the return time
2209 Internal function, called only by AddReturn that calculates and updates
2210 the user fine days, and debars them if necessary.
2212 Should only be called for overdue returns
2214 =cut
2216 sub _debar_user_on_return {
2217 my ( $borrower, $item, $dt_due, $return_date ) = @_;
2219 my $branchcode = _GetCircControlBranch( $item, $borrower );
2221 my $circcontrol = C4::Context->preference('CircControl');
2222 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
2223 { categorycode => $borrower->{categorycode},
2224 itemtype => $item->{itype},
2225 branchcode => $branchcode
2228 my $finedays = $issuing_rule ? $issuing_rule->finedays : undef;
2229 my $unit = $issuing_rule ? $issuing_rule->lengthunit : undef;
2230 my $chargeable_units = C4::Overdues::get_chargeable_units($unit, $dt_due, $return_date, $branchcode);
2232 if ($finedays) {
2234 # finedays is in days, so hourly loans must multiply by 24
2235 # thus 1 hour late equals 1 day suspension * finedays rate
2236 $finedays = $finedays * 24 if ( $unit eq 'hours' );
2238 # grace period is measured in the same units as the loan
2239 my $grace =
2240 DateTime::Duration->new( $unit => $issuing_rule->firstremind );
2242 my $deltadays = DateTime::Duration->new(
2243 days => $chargeable_units
2245 if ( $deltadays->subtract($grace)->is_positive() ) {
2246 my $suspension_days = $deltadays * $finedays;
2248 # If the max suspension days is < than the suspension days
2249 # the suspension days is limited to this maximum period.
2250 my $max_sd = $issuing_rule->maxsuspensiondays;
2251 if ( defined $max_sd ) {
2252 $max_sd = DateTime::Duration->new( days => $max_sd );
2253 $suspension_days = $max_sd
2254 if DateTime::Duration->compare( $max_sd, $suspension_days ) < 0;
2257 my ( $has_been_extended, $is_a_reminder );
2258 if ( C4::Context->preference('CumulativeRestrictionPeriods') and $borrower->{debarred} ) {
2259 my $debarment = @{ GetDebarments( { borrowernumber => $borrower->{borrowernumber}, type => 'SUSPENSION' } ) }[0];
2260 if ( $debarment ) {
2261 $return_date = dt_from_string( $debarment->{expiration}, 'sql' );
2262 $has_been_extended = 1;
2266 my $new_debar_dt =
2267 $return_date->clone()->add_duration( $suspension_days );
2269 Koha::Patron::Debarments::AddUniqueDebarment({
2270 borrowernumber => $borrower->{borrowernumber},
2271 expiration => $new_debar_dt->ymd(),
2272 type => 'SUSPENSION',
2274 # if borrower was already debarred but does not get an extra debarment
2275 my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
2276 my $new_debarment_str;
2277 if ( $borrower->{debarred} eq $patron->is_debarred ) {
2278 $is_a_reminder = 1;
2279 $new_debarment_str = $borrower->{debarred};
2280 } else {
2281 $new_debarment_str = $new_debar_dt->ymd();
2283 # FIXME Should return a DateTime object
2284 return $new_debarment_str, $is_a_reminder;
2287 return;
2290 =head2 _FixOverduesOnReturn
2292 &_FixOverduesOnReturn($brn,$itm, $exemptfine, $dropboxmode);
2294 C<$brn> borrowernumber
2296 C<$itm> itemnumber
2298 C<$exemptfine> BOOL -- remove overdue charge associated with this issue.
2299 C<$dropboxmode> BOOL -- remove lastincrement on overdue charge associated with this issue.
2301 Internal function, called only by AddReturn
2303 =cut
2305 sub _FixOverduesOnReturn {
2306 my ($borrowernumber, $item);
2307 unless ($borrowernumber = shift) {
2308 warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
2309 return;
2311 unless ($item = shift) {
2312 warn "_FixOverduesOnReturn() not supplied valid itemnumber";
2313 return;
2315 my ($exemptfine, $dropbox) = @_;
2316 my $dbh = C4::Context->dbh;
2318 # check for overdue fine
2319 my $sth = $dbh->prepare(
2320 "SELECT * FROM accountlines WHERE (borrowernumber = ?) AND (itemnumber = ?) AND (accounttype='FU' OR accounttype='O')"
2322 $sth->execute( $borrowernumber, $item );
2324 # alter fine to show that the book has been returned
2325 my $data = $sth->fetchrow_hashref;
2326 return 0 unless $data; # no warning, there's just nothing to fix
2328 my $uquery;
2329 my @bind = ($data->{'accountlines_id'});
2330 if ($exemptfine) {
2331 $uquery = "update accountlines set accounttype='FFOR', amountoutstanding=0";
2332 if (C4::Context->preference("FinesLog")) {
2333 &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
2335 } elsif ($dropbox && $data->{lastincrement}) {
2336 my $outstanding = $data->{amountoutstanding} - $data->{lastincrement} ;
2337 my $amt = $data->{amount} - $data->{lastincrement} ;
2338 if (C4::Context->preference("FinesLog")) {
2339 &logaction("FINES", 'MODIFY',$borrowernumber,"Dropbox adjustment $amt, item $item");
2341 $uquery = "update accountlines set accounttype='F' ";
2342 if($outstanding >= 0 && $amt >=0) {
2343 $uquery .= ", amount = ? , amountoutstanding=? ";
2344 unshift @bind, ($amt, $outstanding) ;
2346 } else {
2347 $uquery = "update accountlines set accounttype='F' ";
2349 $uquery .= " where (accountlines_id = ?)";
2350 my $usth = $dbh->prepare($uquery);
2351 return $usth->execute(@bind);
2354 =head2 _FixAccountForLostAndReturned
2356 &_FixAccountForLostAndReturned($itemnumber, [$borrowernumber, $barcode]);
2358 Calculates the charge for a book lost and returned.
2360 Internal function, not exported, called only by AddReturn.
2362 FIXME: This function reflects how inscrutable fines logic is. Fix both.
2363 FIXME: Give a positive return value on success. It might be the $borrowernumber who received credit, or the amount forgiven.
2365 =cut
2367 sub _FixAccountForLostAndReturned {
2368 my $itemnumber = shift or return;
2369 my $borrowernumber = @_ ? shift : undef;
2370 my $item_id = @_ ? shift : $itemnumber; # Send the barcode if you want that logged in the description
2371 my $dbh = C4::Context->dbh;
2372 # check for charge made for lost book
2373 my $sth = $dbh->prepare("SELECT * FROM accountlines WHERE itemnumber = ? AND accounttype IN ('L', 'Rep', 'W') ORDER BY date DESC, accountno DESC");
2374 $sth->execute($itemnumber);
2375 my $data = $sth->fetchrow_hashref;
2376 $data or return; # bail if there is nothing to do
2377 $data->{accounttype} eq 'W' and return; # Written off
2379 # writeoff this amount
2380 my $offset;
2381 my $amount = $data->{'amount'};
2382 my $acctno = $data->{'accountno'};
2383 my $amountleft; # Starts off undef/zero.
2384 if ($data->{'amountoutstanding'} == $amount) {
2385 $offset = $data->{'amount'};
2386 $amountleft = 0; # Hey, it's zero here, too.
2387 } else {
2388 $offset = $amount - $data->{'amountoutstanding'}; # Um, isn't this the same as ZERO? We just tested those two things are ==
2389 $amountleft = $data->{'amountoutstanding'} - $amount; # Um, isn't this the same as ZERO? We just tested those two things are ==
2391 my $usth = $dbh->prepare("UPDATE accountlines SET accounttype = 'LR',amountoutstanding='0'
2392 WHERE (accountlines_id = ?)");
2393 $usth->execute($data->{'accountlines_id'}); # We might be adjusting an account for some OTHER borrowernumber now. Not the one we passed in.
2394 #check if any credit is left if so writeoff other accounts
2395 my $nextaccntno = getnextacctno($data->{'borrowernumber'});
2396 $amountleft *= -1 if ($amountleft < 0);
2397 if ($amountleft > 0) {
2398 my $msth = $dbh->prepare("SELECT * FROM accountlines WHERE (borrowernumber = ?)
2399 AND (amountoutstanding >0) ORDER BY date"); # might want to order by amountoustanding ASC (pay smallest first)
2400 $msth->execute($data->{'borrowernumber'});
2401 # offset transactions
2402 my $newamtos;
2403 my $accdata;
2404 while (($accdata=$msth->fetchrow_hashref) and ($amountleft>0)){
2405 if ($accdata->{'amountoutstanding'} < $amountleft) {
2406 $newamtos = 0;
2407 $amountleft -= $accdata->{'amountoutstanding'};
2408 } else {
2409 $newamtos = $accdata->{'amountoutstanding'} - $amountleft;
2410 $amountleft = 0;
2412 my $thisacct = $accdata->{'accountlines_id'};
2413 # FIXME: move prepares outside while loop!
2414 my $usth = $dbh->prepare("UPDATE accountlines SET amountoutstanding= ?
2415 WHERE (accountlines_id = ?)");
2416 $usth->execute($newamtos,$thisacct);
2417 $usth = $dbh->prepare("INSERT INTO accountoffsets
2418 (borrowernumber, accountno, offsetaccount, offsetamount)
2419 VALUES
2420 (?,?,?,?)");
2421 $usth->execute($data->{'borrowernumber'},$accdata->{'accountno'},$nextaccntno,$newamtos);
2424 $amountleft *= -1 if ($amountleft > 0);
2425 my $desc = "Item Returned " . $item_id;
2426 $usth = $dbh->prepare("INSERT INTO accountlines
2427 (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
2428 VALUES (?,?,now(),?,?,'CR',?)");
2429 $usth->execute($data->{'borrowernumber'},$nextaccntno,0-$amount,$desc,$amountleft);
2430 if ($borrowernumber) {
2431 # FIXME: same as query above. use 1 sth for both
2432 $usth = $dbh->prepare("INSERT INTO accountoffsets
2433 (borrowernumber, accountno, offsetaccount, offsetamount)
2434 VALUES (?,?,?,?)");
2435 $usth->execute($borrowernumber, $data->{'accountno'}, $nextaccntno, $offset);
2437 ModItem({ paidfor => '' }, undef, $itemnumber);
2438 return;
2441 =head2 _GetCircControlBranch
2443 my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
2445 Internal function :
2447 Return the library code to be used to determine which circulation
2448 policy applies to a transaction. Looks up the CircControl and
2449 HomeOrHoldingBranch system preferences.
2451 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
2453 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
2455 =cut
2457 sub _GetCircControlBranch {
2458 my ($item, $borrower) = @_;
2459 my $circcontrol = C4::Context->preference('CircControl');
2460 my $branch;
2462 if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
2463 $branch= C4::Context->userenv->{'branch'};
2464 } elsif ($circcontrol eq 'PatronLibrary') {
2465 $branch=$borrower->{branchcode};
2466 } else {
2467 my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
2468 $branch = $item->{$branchfield};
2469 # default to item home branch if holdingbranch is used
2470 # and is not defined
2471 if (!defined($branch) && $branchfield eq 'holdingbranch') {
2472 $branch = $item->{homebranch};
2475 return $branch;
2478 =head2 GetOpenIssue
2480 $issue = GetOpenIssue( $itemnumber );
2482 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2484 C<$itemnumber> is the item's itemnumber
2486 Returns a hashref
2488 =cut
2490 sub GetOpenIssue {
2491 my ( $itemnumber ) = @_;
2492 return unless $itemnumber;
2493 my $dbh = C4::Context->dbh;
2494 my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2495 $sth->execute( $itemnumber );
2496 return $sth->fetchrow_hashref();
2500 =head2 GetBiblioIssues
2502 $issues = GetBiblioIssues($biblionumber);
2504 this function get all issues from a biblionumber.
2506 Return:
2507 C<$issues> is a reference to array which each value is ref-to-hash. This ref-to-hash containts all column from
2508 tables issues and the firstname,surname & cardnumber from borrowers.
2510 =cut
2512 sub GetBiblioIssues {
2513 my $biblionumber = shift;
2514 return unless $biblionumber;
2515 my $dbh = C4::Context->dbh;
2516 my $query = "
2517 SELECT issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2518 FROM issues
2519 LEFT JOIN borrowers ON borrowers.borrowernumber = issues.borrowernumber
2520 LEFT JOIN items ON issues.itemnumber = items.itemnumber
2521 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2522 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2523 WHERE biblio.biblionumber = ?
2524 UNION ALL
2525 SELECT old_issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2526 FROM old_issues
2527 LEFT JOIN borrowers ON borrowers.borrowernumber = old_issues.borrowernumber
2528 LEFT JOIN items ON old_issues.itemnumber = items.itemnumber
2529 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2530 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2531 WHERE biblio.biblionumber = ?
2532 ORDER BY timestamp
2534 my $sth = $dbh->prepare($query);
2535 $sth->execute($biblionumber, $biblionumber);
2537 my @issues;
2538 while ( my $data = $sth->fetchrow_hashref ) {
2539 push @issues, $data;
2541 return \@issues;
2544 =head2 GetUpcomingDueIssues
2546 my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2548 =cut
2550 sub GetUpcomingDueIssues {
2551 my $params = shift;
2553 $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2554 my $dbh = C4::Context->dbh;
2556 my $statement = <<END_SQL;
2557 SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2558 FROM issues
2559 LEFT JOIN items USING (itemnumber)
2560 LEFT OUTER JOIN branches USING (branchcode)
2561 WHERE returndate is NULL
2562 HAVING days_until_due >= 0 AND days_until_due <= ?
2563 END_SQL
2565 my @bind_parameters = ( $params->{'days_in_advance'} );
2567 my $sth = $dbh->prepare( $statement );
2568 $sth->execute( @bind_parameters );
2569 my $upcoming_dues = $sth->fetchall_arrayref({});
2571 return $upcoming_dues;
2574 =head2 CanBookBeRenewed
2576 ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber[, $override_limit]);
2578 Find out whether a borrowed item may be renewed.
2580 C<$borrowernumber> is the borrower number of the patron who currently
2581 has the item on loan.
2583 C<$itemnumber> is the number of the item to renew.
2585 C<$override_limit>, if supplied with a true value, causes
2586 the limit on the number of times that the loan can be renewed
2587 (as controlled by the item type) to be ignored. Overriding also allows
2588 to renew sooner than "No renewal before" and to manually renew loans
2589 that are automatically renewed.
2591 C<$CanBookBeRenewed> returns a true value if the item may be renewed. The
2592 item must currently be on loan to the specified borrower; renewals
2593 must be allowed for the item's type; and the borrower must not have
2594 already renewed the loan. $error will contain the reason the renewal can not proceed
2596 =cut
2598 sub CanBookBeRenewed {
2599 my ( $borrowernumber, $itemnumber, $override_limit ) = @_;
2601 my $dbh = C4::Context->dbh;
2602 my $renews = 1;
2604 my $item = GetItem($itemnumber) or return ( 0, 'no_item' );
2605 my $issue = Koha::Checkouts->find( { itemnumber => $itemnumber } ) or return ( 0, 'no_checkout' );
2606 return ( 0, 'onsite_checkout' ) if $issue->onsite_checkout;
2608 $borrowernumber ||= $issue->borrowernumber;
2609 my $patron = Koha::Patrons->find( $borrowernumber )
2610 or return;
2612 my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2614 # This item can fill one or more unfilled reserve, can those unfilled reserves
2615 # all be filled by other available items?
2616 if ( $resfound
2617 && C4::Context->preference('AllowRenewalIfOtherItemsAvailable') )
2619 my $schema = Koha::Database->new()->schema();
2621 my $item_holds = $schema->resultset('Reserve')->search( { itemnumber => $itemnumber, found => undef } )->count();
2622 if ($item_holds) {
2623 # There is an item level hold on this item, no other item can fill the hold
2624 $resfound = 1;
2626 else {
2628 # Get all other items that could possibly fill reserves
2629 my @itemnumbers = $schema->resultset('Item')->search(
2631 biblionumber => $resrec->{biblionumber},
2632 onloan => undef,
2633 notforloan => 0,
2634 -not => { itemnumber => $itemnumber }
2636 { columns => 'itemnumber' }
2637 )->get_column('itemnumber')->all();
2639 # Get all other reserves that could have been filled by this item
2640 my @borrowernumbers;
2641 while (1) {
2642 my ( $reserve_found, $reserve, undef ) =
2643 C4::Reserves::CheckReserves( $itemnumber, undef, undef, \@borrowernumbers );
2645 if ($reserve_found) {
2646 push( @borrowernumbers, $reserve->{borrowernumber} );
2648 else {
2649 last;
2653 # If the count of the union of the lists of reservable items for each borrower
2654 # is equal or greater than the number of borrowers, we know that all reserves
2655 # can be filled with available items. We can get the union of the sets simply
2656 # by pushing all the elements onto an array and removing the duplicates.
2657 my @reservable;
2658 my %borrowers;
2659 ITEM: foreach my $i (@itemnumbers) {
2660 my $item = GetItem($i);
2661 next if IsItemOnHoldAndFound($i);
2662 for my $b (@borrowernumbers) {
2663 my $borr = $borrowers{$b} //= Koha::Patrons->find( $b )->unblessed;
2664 next unless IsAvailableForItemLevelRequest($item, $borr);
2665 next unless CanItemBeReserved($b,$i);
2667 push @reservable, $i;
2668 if (@reservable >= @borrowernumbers) {
2669 $resfound = 0;
2670 last ITEM;
2672 last;
2677 return ( 0, "on_reserve" ) if $resfound; # '' when no hold was found
2679 return ( 1, undef ) if $override_limit;
2681 my $branchcode = _GetCircControlBranch( $item, $patron->unblessed );
2682 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
2683 { categorycode => $patron->categorycode,
2684 itemtype => $item->{itype},
2685 branchcode => $branchcode
2689 return ( 0, "too_many" )
2690 if not $issuing_rule or $issuing_rule->renewalsallowed <= $issue->renewals;
2692 my $overduesblockrenewing = C4::Context->preference('OverduesBlockRenewing');
2693 my $restrictionblockrenewing = C4::Context->preference('RestrictionBlockRenewing');
2694 $patron = Koha::Patrons->find($borrowernumber); # FIXME Is this really useful?
2695 my $restricted = $patron->is_debarred;
2696 my $hasoverdues = $patron->has_overdues;
2698 if ( $restricted and $restrictionblockrenewing ) {
2699 return ( 0, 'restriction');
2700 } elsif ( ($hasoverdues and $overduesblockrenewing eq 'block') || ($issue->is_overdue and $overduesblockrenewing eq 'blockitem') ) {
2701 return ( 0, 'overdue');
2704 if ( $issue->auto_renew ) {
2705 if ( defined $issuing_rule->no_auto_renewal_after
2706 and $issuing_rule->no_auto_renewal_after ne "" ) {
2707 # Get issue_date and add no_auto_renewal_after
2708 # If this is greater than today, it's too late for renewal.
2709 my $maximum_renewal_date = dt_from_string($issue->issuedate, 'sql');
2710 $maximum_renewal_date->add(
2711 $issuing_rule->lengthunit => $issuing_rule->no_auto_renewal_after
2713 my $now = dt_from_string;
2714 if ( $now >= $maximum_renewal_date ) {
2715 return ( 0, "auto_too_late" );
2718 if ( defined $issuing_rule->no_auto_renewal_after_hard_limit
2719 and $issuing_rule->no_auto_renewal_after_hard_limit ne "" ) {
2720 # If no_auto_renewal_after_hard_limit is >= today, it's also too late for renewal
2721 if ( dt_from_string >= dt_from_string( $issuing_rule->no_auto_renewal_after_hard_limit ) ) {
2722 return ( 0, "auto_too_late" );
2726 if ( C4::Context->preference('OPACFineNoRenewalsBlockAutoRenew') ) {
2727 my $fine_no_renewals = C4::Context->preference("OPACFineNoRenewals");
2728 my ( $amountoutstanding ) = C4::Members::GetMemberAccountRecords($patron->borrowernumber);
2729 if ( $amountoutstanding and $amountoutstanding > $fine_no_renewals ) {
2730 return ( 0, "auto_too_much_oweing" );
2735 if ( defined $issuing_rule->norenewalbefore
2736 and $issuing_rule->norenewalbefore ne "" )
2739 # Calculate soonest renewal by subtracting 'No renewal before' from due date
2740 my $soonestrenewal = dt_from_string( $issue->date_due, 'sql' )->subtract(
2741 $issuing_rule->lengthunit => $issuing_rule->norenewalbefore );
2743 # Depending on syspref reset the exact time, only check the date
2744 if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
2745 and $issuing_rule->lengthunit eq 'days' )
2747 $soonestrenewal->truncate( to => 'day' );
2750 if ( $soonestrenewal > DateTime->now( time_zone => C4::Context->tz() ) )
2752 return ( 0, "auto_too_soon" ) if $issue->auto_renew;
2753 return ( 0, "too_soon" );
2755 elsif ( $issue->auto_renew ) {
2756 return ( 0, "auto_renew" );
2760 # Fallback for automatic renewals:
2761 # If norenewalbefore is undef, don't renew before due date.
2762 if ( $issue->auto_renew ) {
2763 my $now = dt_from_string;
2764 return ( 0, "auto_renew" )
2765 if $now >= dt_from_string( $issue->date_due, 'sql' );
2766 return ( 0, "auto_too_soon" );
2769 return ( 1, undef );
2772 =head2 AddRenewal
2774 &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
2776 Renews a loan.
2778 C<$borrowernumber> is the borrower number of the patron who currently
2779 has the item.
2781 C<$itemnumber> is the number of the item to renew.
2783 C<$branch> is the library where the renewal took place (if any).
2784 The library that controls the circ policies for the renewal is retrieved from the issues record.
2786 C<$datedue> can be a DateTime object used to set the due date.
2788 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate. If
2789 this parameter is not supplied, lastreneweddate is set to the current date.
2791 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2792 from the book's item type.
2794 =cut
2796 sub AddRenewal {
2797 my $borrowernumber = shift;
2798 my $itemnumber = shift or return;
2799 my $branch = shift;
2800 my $datedue = shift;
2801 my $lastreneweddate = shift || DateTime->now(time_zone => C4::Context->tz)->ymd();
2803 my $item = GetItem($itemnumber) or return;
2804 my $item_object = Koha::Items->find( $itemnumber ); # Should replace $item
2805 my $biblio = $item_object->biblio;
2807 my $dbh = C4::Context->dbh;
2809 # Find the issues record for this book
2810 my $issue = Koha::Checkouts->find( { itemnumber => $itemnumber } );
2812 return unless $issue;
2814 $borrowernumber ||= $issue->borrowernumber;
2816 if ( defined $datedue && ref $datedue ne 'DateTime' ) {
2817 carp 'Invalid date passed to AddRenewal.';
2818 return;
2821 my $patron = Koha::Patrons->find( $borrowernumber ) or return; # FIXME Should do more than just return
2822 my $patron_unblessed = $patron->unblessed;
2824 if ( C4::Context->preference('CalculateFinesOnReturn') && $issue->is_overdue ) {
2825 _CalculateAndUpdateFine( { issue => $issue, item => $item, borrower => $patron_unblessed } );
2827 _FixOverduesOnReturn( $borrowernumber, $itemnumber );
2829 # If the due date wasn't specified, calculate it by adding the
2830 # book's loan length to today's date or the current due date
2831 # based on the value of the RenewalPeriodBase syspref.
2832 unless ($datedue) {
2834 my $itemtype = $item_object->effective_itemtype;
2835 $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
2836 dt_from_string( $issue->date_due, 'sql' ) :
2837 DateTime->now( time_zone => C4::Context->tz());
2838 $datedue = CalcDateDue($datedue, $itemtype, _GetCircControlBranch($item, $patron_unblessed), $patron_unblessed, 'is a renewal');
2841 # Update the issues record to have the new due date, and a new count
2842 # of how many times it has been renewed.
2843 my $renews = $issue->renewals + 1;
2844 my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
2845 WHERE borrowernumber=?
2846 AND itemnumber=?"
2849 $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
2851 # Update the renewal count on the item, and tell zebra to reindex
2852 $renews = $item->{renewals} + 1;
2853 ModItem({ renewals => $renews, onloan => $datedue->strftime('%Y-%m-%d %H:%M')}, $item->{biblionumber}, $itemnumber);
2855 # Charge a new rental fee, if applicable?
2856 my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
2857 if ( $charge > 0 ) {
2858 my $accountno = getnextacctno( $borrowernumber );
2859 my $manager_id = 0;
2860 $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
2861 $sth = $dbh->prepare(
2862 "INSERT INTO accountlines
2863 (date, borrowernumber, accountno, amount, manager_id,
2864 description,accounttype, amountoutstanding, itemnumber)
2865 VALUES (now(),?,?,?,?,?,?,?,?)"
2867 $sth->execute( $borrowernumber, $accountno, $charge, $manager_id,
2868 "Renewal of Rental Item " . $biblio->title . " $item->{'barcode'}",
2869 'Rent', $charge, $itemnumber );
2872 # Send a renewal slip according to checkout alert preferencei
2873 if ( C4::Context->preference('RenewalSendNotice') eq '1' ) {
2874 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2875 my %conditions = (
2876 branchcode => $branch,
2877 categorycode => $patron->categorycode,
2878 item_type => $item->{itype},
2879 notification => 'CHECKOUT',
2881 if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
2882 SendCirculationAlert(
2884 type => 'RENEWAL',
2885 item => $item,
2886 borrower => $patron->unblessed,
2887 branch => $branch,
2893 # Remove any OVERDUES related debarment if the borrower has no overdues
2894 if ( $patron
2895 && $patron->is_debarred
2896 && ! $patron->has_overdues
2897 && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2899 DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2902 unless ( C4::Context->interface eq 'opac' ) { #if from opac we are obeying OpacRenewalBranch as calculated in opac-renew.pl
2903 $branch = C4::Context->userenv ? C4::Context->userenv->{branch} : $branch;
2906 # Add the renewal to stats
2907 UpdateStats(
2909 branch => $branch,
2910 type => 'renew',
2911 amount => $charge,
2912 itemnumber => $itemnumber,
2913 itemtype => $item->{itype},
2914 borrowernumber => $borrowernumber,
2915 ccode => $item->{'ccode'}
2919 #Log the renewal
2920 logaction("CIRCULATION", "RENEWAL", $borrowernumber, $itemnumber) if C4::Context->preference("RenewalLog");
2921 return $datedue;
2924 sub GetRenewCount {
2925 # check renewal status
2926 my ( $bornum, $itemno ) = @_;
2927 my $dbh = C4::Context->dbh;
2928 my $renewcount = 0;
2929 my $renewsallowed = 0;
2930 my $renewsleft = 0;
2932 my $patron = Koha::Patrons->find( $bornum );
2933 my $item = GetItem($itemno);
2935 return (0, 0, 0) unless $patron or $item; # Wrong call, no renewal allowed
2937 # Look in the issues table for this item, lent to this borrower,
2938 # and not yet returned.
2940 # FIXME - I think this function could be redone to use only one SQL call.
2941 my $sth = $dbh->prepare(
2942 "select * from issues
2943 where (borrowernumber = ?)
2944 and (itemnumber = ?)"
2946 $sth->execute( $bornum, $itemno );
2947 my $data = $sth->fetchrow_hashref;
2948 $renewcount = $data->{'renewals'} if $data->{'renewals'};
2949 # $item and $borrower should be calculated
2950 my $branchcode = _GetCircControlBranch($item, $patron->unblessed);
2952 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
2953 { categorycode => $patron->categorycode,
2954 itemtype => $item->{itype},
2955 branchcode => $branchcode
2959 $renewsallowed = $issuing_rule ? $issuing_rule->renewalsallowed : 0;
2960 $renewsleft = $renewsallowed - $renewcount;
2961 if($renewsleft < 0){ $renewsleft = 0; }
2962 return ( $renewcount, $renewsallowed, $renewsleft );
2965 =head2 GetSoonestRenewDate
2967 $NoRenewalBeforeThisDate = &GetSoonestRenewDate($borrowernumber, $itemnumber);
2969 Find out the soonest possible renew date of a borrowed item.
2971 C<$borrowernumber> is the borrower number of the patron who currently
2972 has the item on loan.
2974 C<$itemnumber> is the number of the item to renew.
2976 C<$GetSoonestRenewDate> returns the DateTime of the soonest possible
2977 renew date, based on the value "No renewal before" of the applicable
2978 issuing rule. Returns the current date if the item can already be
2979 renewed, and returns undefined if the borrower, loan, or item
2980 cannot be found.
2982 =cut
2984 sub GetSoonestRenewDate {
2985 my ( $borrowernumber, $itemnumber ) = @_;
2987 my $dbh = C4::Context->dbh;
2989 my $item = GetItem($itemnumber) or return;
2990 my $itemissue = Koha::Checkouts->find( { itemnumber => $itemnumber } ) or return;
2992 $borrowernumber ||= $itemissue->borrowernumber;
2993 my $patron = Koha::Patrons->find( $borrowernumber )
2994 or return;
2996 my $branchcode = _GetCircControlBranch( $item, $patron->unblessed );
2997 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
2998 { categorycode => $patron->categorycode,
2999 itemtype => $item->{itype},
3000 branchcode => $branchcode
3004 my $now = dt_from_string;
3005 return $now unless $issuing_rule;
3007 if ( defined $issuing_rule->norenewalbefore
3008 and $issuing_rule->norenewalbefore ne "" )
3010 my $soonestrenewal =
3011 dt_from_string( $itemissue->date_due )->subtract(
3012 $issuing_rule->lengthunit => $issuing_rule->norenewalbefore );
3014 if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
3015 and $issuing_rule->lengthunit eq 'days' )
3017 $soonestrenewal->truncate( to => 'day' );
3019 return $soonestrenewal if $now < $soonestrenewal;
3021 return $now;
3024 =head2 GetLatestAutoRenewDate
3026 $NoAutoRenewalAfterThisDate = &GetLatestAutoRenewDate($borrowernumber, $itemnumber);
3028 Find out the latest possible auto renew date of a borrowed item.
3030 C<$borrowernumber> is the borrower number of the patron who currently
3031 has the item on loan.
3033 C<$itemnumber> is the number of the item to renew.
3035 C<$GetLatestAutoRenewDate> returns the DateTime of the latest possible
3036 auto renew date, based on the value "No auto renewal after" and the "No auto
3037 renewal after (hard limit) of the applicable issuing rule.
3038 Returns undef if there is no date specify in the circ rules or if the patron, loan,
3039 or item cannot be found.
3041 =cut
3043 sub GetLatestAutoRenewDate {
3044 my ( $borrowernumber, $itemnumber ) = @_;
3046 my $dbh = C4::Context->dbh;
3048 my $item = GetItem($itemnumber) or return;
3049 my $itemissue = Koha::Checkouts->find( { itemnumber => $itemnumber } ) or return;
3051 $borrowernumber ||= $itemissue->borrowernumber;
3052 my $patron = Koha::Patrons->find( $borrowernumber )
3053 or return;
3055 my $branchcode = _GetCircControlBranch( $item, $patron->unblessed );
3056 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
3057 { categorycode => $patron->categorycode,
3058 itemtype => $item->{itype},
3059 branchcode => $branchcode
3063 return unless $issuing_rule;
3064 return
3065 if ( not $issuing_rule->no_auto_renewal_after
3066 or $issuing_rule->no_auto_renewal_after eq '' )
3067 and ( not $issuing_rule->no_auto_renewal_after_hard_limit
3068 or $issuing_rule->no_auto_renewal_after_hard_limit eq '' );
3070 my $maximum_renewal_date;
3071 if ( $issuing_rule->no_auto_renewal_after ) {
3072 $maximum_renewal_date = dt_from_string($itemissue->issuedate);
3073 $maximum_renewal_date->add(
3074 $issuing_rule->lengthunit => $issuing_rule->no_auto_renewal_after
3078 if ( $issuing_rule->no_auto_renewal_after_hard_limit ) {
3079 my $dt = dt_from_string( $issuing_rule->no_auto_renewal_after_hard_limit );
3080 $maximum_renewal_date = $dt if not $maximum_renewal_date or $maximum_renewal_date > $dt;
3082 return $maximum_renewal_date;
3086 =head2 GetIssuingCharges
3088 ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
3090 Calculate how much it would cost for a given patron to borrow a given
3091 item, including any applicable discounts.
3093 C<$itemnumber> is the item number of item the patron wishes to borrow.
3095 C<$borrowernumber> is the patron's borrower number.
3097 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
3098 and C<$item_type> is the code for the item's item type (e.g., C<VID>
3099 if it's a video).
3101 =cut
3103 sub GetIssuingCharges {
3105 # calculate charges due
3106 my ( $itemnumber, $borrowernumber ) = @_;
3107 my $charge = 0;
3108 my $dbh = C4::Context->dbh;
3109 my $item_type;
3111 # Get the book's item type and rental charge (via its biblioitem).
3112 my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
3113 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
3114 $charge_query .= (C4::Context->preference('item-level_itypes'))
3115 ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
3116 : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
3118 $charge_query .= ' WHERE items.itemnumber =?';
3120 my $sth = $dbh->prepare($charge_query);
3121 $sth->execute($itemnumber);
3122 if ( my $item_data = $sth->fetchrow_hashref ) {
3123 $item_type = $item_data->{itemtype};
3124 $charge = $item_data->{rentalcharge};
3125 my $branch = C4::Context::mybranch();
3126 my $discount_query = q|SELECT rentaldiscount,
3127 issuingrules.itemtype, issuingrules.branchcode
3128 FROM borrowers
3129 LEFT JOIN issuingrules ON borrowers.categorycode = issuingrules.categorycode
3130 WHERE borrowers.borrowernumber = ?
3131 AND (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
3132 AND (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')|;
3133 my $discount_sth = $dbh->prepare($discount_query);
3134 $discount_sth->execute( $borrowernumber, $item_type, $branch );
3135 my $discount_rules = $discount_sth->fetchall_arrayref({});
3136 if (@{$discount_rules}) {
3137 # We may have multiple rules so get the most specific
3138 my $discount = _get_discount_from_rule($discount_rules, $branch, $item_type);
3139 $charge = ( $charge * ( 100 - $discount ) ) / 100;
3141 if ($charge) {
3142 $charge = sprintf '%.2f', $charge; # ensure no fractions of a penny returned
3146 return ( $charge, $item_type );
3149 # Select most appropriate discount rule from those returned
3150 sub _get_discount_from_rule {
3151 my ($rules_ref, $branch, $itemtype) = @_;
3152 my $discount;
3154 if (@{$rules_ref} == 1) { # only 1 applicable rule use it
3155 $discount = $rules_ref->[0]->{rentaldiscount};
3156 return (defined $discount) ? $discount : 0;
3158 # could have up to 4 does one match $branch and $itemtype
3159 my @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq $itemtype } @{$rules_ref};
3160 if (@d) {
3161 $discount = $d[0]->{rentaldiscount};
3162 return (defined $discount) ? $discount : 0;
3164 # do we have item type + all branches
3165 @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq $itemtype } @{$rules_ref};
3166 if (@d) {
3167 $discount = $d[0]->{rentaldiscount};
3168 return (defined $discount) ? $discount : 0;
3170 # do we all item types + this branch
3171 @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq q{*} } @{$rules_ref};
3172 if (@d) {
3173 $discount = $d[0]->{rentaldiscount};
3174 return (defined $discount) ? $discount : 0;
3176 # so all and all (surely we wont get here)
3177 @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq q{*} } @{$rules_ref};
3178 if (@d) {
3179 $discount = $d[0]->{rentaldiscount};
3180 return (defined $discount) ? $discount : 0;
3182 # none of the above
3183 return 0;
3186 =head2 AddIssuingCharge
3188 &AddIssuingCharge( $itemno, $borrowernumber, $charge )
3190 =cut
3192 sub AddIssuingCharge {
3193 my ( $itemnumber, $borrowernumber, $charge ) = @_;
3194 my $dbh = C4::Context->dbh;
3195 my $nextaccntno = getnextacctno( $borrowernumber );
3196 my $manager_id = 0;
3197 $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
3198 my $query ="
3199 INSERT INTO accountlines
3200 (borrowernumber, itemnumber, accountno,
3201 date, amount, description, accounttype,
3202 amountoutstanding, manager_id)
3203 VALUES (?, ?, ?,now(), ?, 'Rental', 'Rent',?,?)
3205 my $sth = $dbh->prepare($query);
3206 $sth->execute( $borrowernumber, $itemnumber, $nextaccntno, $charge, $charge, $manager_id );
3209 =head2 GetTransfers
3211 GetTransfers($itemnumber);
3213 =cut
3215 sub GetTransfers {
3216 my ($itemnumber) = @_;
3218 my $dbh = C4::Context->dbh;
3220 my $query = '
3221 SELECT datesent,
3222 frombranch,
3223 tobranch,
3224 branchtransfer_id
3225 FROM branchtransfers
3226 WHERE itemnumber = ?
3227 AND datearrived IS NULL
3229 my $sth = $dbh->prepare($query);
3230 $sth->execute($itemnumber);
3231 my @row = $sth->fetchrow_array();
3232 return @row;
3235 =head2 GetTransfersFromTo
3237 @results = GetTransfersFromTo($frombranch,$tobranch);
3239 Returns the list of pending transfers between $from and $to branch
3241 =cut
3243 sub GetTransfersFromTo {
3244 my ( $frombranch, $tobranch ) = @_;
3245 return unless ( $frombranch && $tobranch );
3246 my $dbh = C4::Context->dbh;
3247 my $query = "
3248 SELECT branchtransfer_id,itemnumber,datesent,frombranch
3249 FROM branchtransfers
3250 WHERE frombranch=?
3251 AND tobranch=?
3252 AND datearrived IS NULL
3254 my $sth = $dbh->prepare($query);
3255 $sth->execute( $frombranch, $tobranch );
3256 my @gettransfers;
3258 while ( my $data = $sth->fetchrow_hashref ) {
3259 push @gettransfers, $data;
3261 return (@gettransfers);
3264 =head2 DeleteTransfer
3266 &DeleteTransfer($itemnumber);
3268 =cut
3270 sub DeleteTransfer {
3271 my ($itemnumber) = @_;
3272 return unless $itemnumber;
3273 my $dbh = C4::Context->dbh;
3274 my $sth = $dbh->prepare(
3275 "DELETE FROM branchtransfers
3276 WHERE itemnumber=?
3277 AND datearrived IS NULL "
3279 return $sth->execute($itemnumber);
3282 =head2 SendCirculationAlert
3284 Send out a C<check-in> or C<checkout> alert using the messaging system.
3286 B<Parameters>:
3288 =over 4
3290 =item type
3292 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
3294 =item item
3296 Hashref of information about the item being checked in or out.
3298 =item borrower
3300 Hashref of information about the borrower of the item.
3302 =item branch
3304 The branchcode from where the checkout or check-in took place.
3306 =back
3308 B<Example>:
3310 SendCirculationAlert({
3311 type => 'CHECKOUT',
3312 item => $item,
3313 borrower => $borrower,
3314 branch => $branch,
3317 =cut
3319 sub SendCirculationAlert {
3320 my ($opts) = @_;
3321 my ($type, $item, $borrower, $branch) =
3322 ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
3323 my %message_name = (
3324 CHECKIN => 'Item_Check_in',
3325 CHECKOUT => 'Item_Checkout',
3326 RENEWAL => 'Item_Checkout',
3328 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
3329 borrowernumber => $borrower->{borrowernumber},
3330 message_name => $message_name{$type},
3332 my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues';
3334 my $schema = Koha::Database->new->schema;
3335 my @transports = keys %{ $borrower_preferences->{transports} };
3337 # From the MySQL doc:
3338 # LOCK TABLES is not transaction-safe and implicitly commits any active transaction before attempting to lock the tables.
3339 # If the LOCK/UNLOCK statements are executed from tests, the current transaction will be committed.
3340 # To avoid that we need to guess if this code is execute from tests or not (yes it is a bit hacky)
3341 my $do_not_lock = ( exists $ENV{_} && $ENV{_} =~ m|prove| ) || $ENV{KOHA_NO_TABLE_LOCKS};
3343 for my $mtt (@transports) {
3344 my $letter = C4::Letters::GetPreparedLetter (
3345 module => 'circulation',
3346 letter_code => $type,
3347 branchcode => $branch,
3348 message_transport_type => $mtt,
3349 lang => $borrower->{lang},
3350 tables => {
3351 $issues_table => $item->{itemnumber},
3352 'items' => $item->{itemnumber},
3353 'biblio' => $item->{biblionumber},
3354 'biblioitems' => $item->{biblionumber},
3355 'borrowers' => $borrower,
3356 'branches' => $branch,
3358 ) or next;
3360 $schema->storage->txn_begin;
3361 C4::Context->dbh->do(q|LOCK TABLE message_queue READ|) unless $do_not_lock;
3362 C4::Context->dbh->do(q|LOCK TABLE message_queue WRITE|) unless $do_not_lock;
3363 my $message = C4::Message->find_last_message($borrower, $type, $mtt);
3364 unless ( $message ) {
3365 C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3366 C4::Message->enqueue($letter, $borrower, $mtt);
3367 } else {
3368 $message->append($letter);
3369 $message->update;
3371 C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3372 $schema->storage->txn_commit;
3375 return;
3378 =head2 updateWrongTransfer
3380 $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
3382 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
3384 =cut
3386 sub updateWrongTransfer {
3387 my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
3388 my $dbh = C4::Context->dbh;
3389 # first step validate the actual line of transfert .
3390 my $sth =
3391 $dbh->prepare(
3392 "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
3394 $sth->execute($FromLibrary,$itemNumber);
3396 # second step create a new line of branchtransfer to the right location .
3397 ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
3399 #third step changing holdingbranch of item
3400 UpdateHoldingbranch($FromLibrary,$itemNumber);
3403 =head2 UpdateHoldingbranch
3405 $items = UpdateHoldingbranch($branch,$itmenumber);
3407 Simple methode for updating hodlingbranch in items BDD line
3409 =cut
3411 sub UpdateHoldingbranch {
3412 my ( $branch,$itemnumber ) = @_;
3413 ModItem({ holdingbranch => $branch }, undef, $itemnumber);
3416 =head2 CalcDateDue
3418 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
3420 this function calculates the due date given the start date and configured circulation rules,
3421 checking against the holidays calendar as per the 'useDaysMode' syspref.
3422 C<$startdate> = DateTime object representing start date of loan period (assumed to be today)
3423 C<$itemtype> = itemtype code of item in question
3424 C<$branch> = location whose calendar to use
3425 C<$borrower> = Borrower object
3426 C<$isrenewal> = Boolean: is true if we want to calculate the date due for a renewal. Else is false.
3428 =cut
3430 sub CalcDateDue {
3431 my ( $startdate, $itemtype, $branch, $borrower, $isrenewal ) = @_;
3433 $isrenewal ||= 0;
3435 # loanlength now a href
3436 my $loanlength =
3437 GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
3439 my $length_key = ( $isrenewal and defined $loanlength->{renewalperiod} )
3440 ? qq{renewalperiod}
3441 : qq{issuelength};
3443 my $datedue;
3444 if ( $startdate ) {
3445 if (ref $startdate ne 'DateTime' ) {
3446 $datedue = dt_from_string($datedue);
3447 } else {
3448 $datedue = $startdate->clone;
3450 } else {
3451 $datedue =
3452 DateTime->now( time_zone => C4::Context->tz() )
3453 ->truncate( to => 'minute' );
3457 # calculate the datedue as normal
3458 if ( C4::Context->preference('useDaysMode') eq 'Days' )
3459 { # ignoring calendar
3460 if ( $loanlength->{lengthunit} eq 'hours' ) {
3461 $datedue->add( hours => $loanlength->{$length_key} );
3462 } else { # days
3463 $datedue->add( days => $loanlength->{$length_key} );
3464 $datedue->set_hour(23);
3465 $datedue->set_minute(59);
3467 } else {
3468 my $dur;
3469 if ($loanlength->{lengthunit} eq 'hours') {
3470 $dur = DateTime::Duration->new( hours => $loanlength->{$length_key});
3472 else { # days
3473 $dur = DateTime::Duration->new( days => $loanlength->{$length_key});
3475 my $calendar = Koha::Calendar->new( branchcode => $branch );
3476 $datedue = $calendar->addDate( $datedue, $dur, $loanlength->{lengthunit} );
3477 if ($loanlength->{lengthunit} eq 'days') {
3478 $datedue->set_hour(23);
3479 $datedue->set_minute(59);
3483 # if Hard Due Dates are used, retrieve them and apply as necessary
3484 my ( $hardduedate, $hardduedatecompare ) =
3485 GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
3486 if ($hardduedate) { # hardduedates are currently dates
3487 $hardduedate->truncate( to => 'minute' );
3488 $hardduedate->set_hour(23);
3489 $hardduedate->set_minute(59);
3490 my $cmp = DateTime->compare( $hardduedate, $datedue );
3492 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
3493 # if the calculated date is before the 'after' Hard Due Date (floor), override
3494 # if the hard due date is set to 'exactly', overrride
3495 if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
3496 $datedue = $hardduedate->clone;
3499 # in all other cases, keep the date due as it is
3503 # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3504 if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3505 my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso', 'floating');
3506 if( $expiry_dt ) { #skip empty expiry date..
3507 $expiry_dt->set( hour => 23, minute => 59);
3508 my $d1= $datedue->clone->set_time_zone('floating');
3509 if ( DateTime->compare( $d1, $expiry_dt ) == 1 ) {
3510 $datedue = $expiry_dt->clone->set_time_zone( C4::Context->tz );
3513 if ( C4::Context->preference('useDaysMode') ne 'Days' ) {
3514 my $calendar = Koha::Calendar->new( branchcode => $branch );
3515 if ( $calendar->is_holiday($datedue) ) {
3516 # Don't return on a closed day
3517 $datedue = $calendar->prev_open_day( $datedue );
3522 return $datedue;
3526 sub CheckValidBarcode{
3527 my ($barcode) = @_;
3528 my $dbh = C4::Context->dbh;
3529 my $query=qq|SELECT count(*)
3530 FROM items
3531 WHERE barcode=?
3533 my $sth = $dbh->prepare($query);
3534 $sth->execute($barcode);
3535 my $exist=$sth->fetchrow ;
3536 return $exist;
3539 =head2 IsBranchTransferAllowed
3541 $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3543 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3545 =cut
3547 sub IsBranchTransferAllowed {
3548 my ( $toBranch, $fromBranch, $code ) = @_;
3550 if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3552 my $limitType = C4::Context->preference("BranchTransferLimitsType");
3553 my $dbh = C4::Context->dbh;
3555 my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3556 $sth->execute( $toBranch, $fromBranch, $code );
3557 my $limit = $sth->fetchrow_hashref();
3559 ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3560 if ( $limit->{'limitId'} ) {
3561 return 0;
3562 } else {
3563 return 1;
3567 =head2 CreateBranchTransferLimit
3569 CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3571 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3573 =cut
3575 sub CreateBranchTransferLimit {
3576 my ( $toBranch, $fromBranch, $code ) = @_;
3577 return unless defined($toBranch) && defined($fromBranch);
3578 my $limitType = C4::Context->preference("BranchTransferLimitsType");
3580 my $dbh = C4::Context->dbh;
3582 my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3583 return $sth->execute( $code, $toBranch, $fromBranch );
3586 =head2 DeleteBranchTransferLimits
3588 my $result = DeleteBranchTransferLimits($frombranch);
3590 Deletes all the library transfer limits for one library. Returns the
3591 number of limits deleted, 0e0 if no limits were deleted, or undef if
3592 no arguments are supplied.
3594 =cut
3596 sub DeleteBranchTransferLimits {
3597 my $branch = shift;
3598 return unless defined $branch;
3599 my $dbh = C4::Context->dbh;
3600 my $sth = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3601 return $sth->execute($branch);
3604 sub ReturnLostItem{
3605 my ( $borrowernumber, $itemnum ) = @_;
3607 MarkIssueReturned( $borrowernumber, $itemnum );
3608 my $patron = Koha::Patrons->find( $borrowernumber );
3609 my $item = C4::Items::GetItem( $itemnum );
3610 my $old_note = ($item->{'paidfor'} && ($item->{'paidfor'} ne q{})) ? $item->{'paidfor'}.' / ' : q{};
3611 my @datearr = localtime(time);
3612 my $date = ( 1900 + $datearr[5] ) . "-" . ( $datearr[4] + 1 ) . "-" . $datearr[3];
3613 my $bor = $patron->firstname . ' ' . $patron->surname . ' ' . $patron->cardnumber;
3614 ModItem({ paidfor => $old_note."Paid for by $bor $date" }, undef, $itemnum);
3618 sub LostItem{
3619 my ($itemnumber, $mark_returned) = @_;
3621 my $dbh = C4::Context->dbh();
3622 my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title
3623 FROM issues
3624 JOIN items USING (itemnumber)
3625 JOIN biblio USING (biblionumber)
3626 WHERE issues.itemnumber=?");
3627 $sth->execute($itemnumber);
3628 my $issues=$sth->fetchrow_hashref();
3630 # If a borrower lost the item, add a replacement cost to the their record
3631 if ( my $borrowernumber = $issues->{borrowernumber} ){
3632 my $patron = Koha::Patrons->find( $borrowernumber );
3634 if (C4::Context->preference('WhenLostForgiveFine')){
3635 my $fix = _FixOverduesOnReturn($borrowernumber, $itemnumber, 1, 0); # 1, 0 = exemptfine, no-dropbox
3636 defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $itemnumber...) failed!"; # zero is OK, check defined
3638 if (C4::Context->preference('WhenLostChargeReplacementFee')){
3639 C4::Accounts::chargelostitem($borrowernumber, $itemnumber, $issues->{'replacementprice'}, "Lost Item $issues->{'title'} $issues->{'barcode'}");
3640 #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3641 #warn " $issues->{'borrowernumber'} / $itemnumber ";
3644 MarkIssueReturned($borrowernumber,$itemnumber,undef,undef,$patron->privacy) if $mark_returned;
3648 sub GetOfflineOperations {
3649 my $dbh = C4::Context->dbh;
3650 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3651 $sth->execute(C4::Context->userenv->{'branch'});
3652 my $results = $sth->fetchall_arrayref({});
3653 return $results;
3656 sub GetOfflineOperation {
3657 my $operationid = shift;
3658 return unless $operationid;
3659 my $dbh = C4::Context->dbh;
3660 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3661 $sth->execute( $operationid );
3662 return $sth->fetchrow_hashref;
3665 sub AddOfflineOperation {
3666 my ( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount ) = @_;
3667 my $dbh = C4::Context->dbh;
3668 my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber, amount) VALUES(?,?,?,?,?,?,?)");
3669 $sth->execute( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount );
3670 return "Added.";
3673 sub DeleteOfflineOperation {
3674 my $dbh = C4::Context->dbh;
3675 my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3676 $sth->execute( shift );
3677 return "Deleted.";
3680 sub ProcessOfflineOperation {
3681 my $operation = shift;
3683 my $report;
3684 if ( $operation->{action} eq 'return' ) {
3685 $report = ProcessOfflineReturn( $operation );
3686 } elsif ( $operation->{action} eq 'issue' ) {
3687 $report = ProcessOfflineIssue( $operation );
3688 } elsif ( $operation->{action} eq 'payment' ) {
3689 $report = ProcessOfflinePayment( $operation );
3692 DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3694 return $report;
3697 sub ProcessOfflineReturn {
3698 my $operation = shift;
3700 my $item = Koha::Items->find({barcode => $operation->{barcode}});
3702 if ( $item ) {
3703 my $itemnumber = $item->itemnumber;
3704 my $issue = GetOpenIssue( $itemnumber );
3705 if ( $issue ) {
3706 MarkIssueReturned(
3707 $issue->{borrowernumber},
3708 $itemnumber,
3709 undef,
3710 $operation->{timestamp},
3712 ModItem(
3713 { renewals => 0, onloan => undef },
3714 $issue->{'biblionumber'},
3715 $itemnumber
3717 return "Success.";
3718 } else {
3719 return "Item not issued.";
3721 } else {
3722 return "Item not found.";
3726 sub ProcessOfflineIssue {
3727 my $operation = shift;
3729 my $patron = Koha::Patrons->find( { cardnumber => $operation->{cardnumber} } );
3731 if ( $patron ) {
3732 my $item = Koha::Items->find({ barcode => $operation->{barcode} });
3733 unless ($item) {
3734 return "Barcode not found.";
3736 my $itemnumber = $item->itemnumber;
3737 my $issue = GetOpenIssue( $itemnumber );
3739 if ( $issue and ( $issue->{borrowernumber} ne $patron->borrowernumber ) ) { # Item already issued to another patron mark it returned
3740 MarkIssueReturned(
3741 $issue->{borrowernumber},
3742 $itemnumber,
3743 undef,
3744 $operation->{timestamp},
3747 AddIssue(
3748 $patron->unblessed,
3749 $operation->{'barcode'},
3750 undef,
3752 $operation->{timestamp},
3753 undef,
3755 return "Success.";
3756 } else {
3757 return "Borrower not found.";
3761 sub ProcessOfflinePayment {
3762 my $operation = shift;
3764 my $patron = Koha::Patrons->find( { cardnumber => $operation->{cardnumber} });
3765 my $amount = $operation->{amount};
3767 Koha::Account->new( { patron_id => $patron->id } )->pay( { amount => $amount } );
3769 return "Success."
3773 =head2 TransferSlip
3775 TransferSlip($user_branch, $itemnumber, $barcode, $to_branch)
3777 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
3779 =cut
3781 sub TransferSlip {
3782 my ($branch, $itemnumber, $barcode, $to_branch) = @_;
3784 my $item = GetItem( $itemnumber, $barcode )
3785 or return;
3787 return C4::Letters::GetPreparedLetter (
3788 module => 'circulation',
3789 letter_code => 'TRANSFERSLIP',
3790 branchcode => $branch,
3791 tables => {
3792 'branches' => $to_branch,
3793 'biblio' => $item->{biblionumber},
3794 'items' => $item,
3799 =head2 CheckIfIssuedToPatron
3801 CheckIfIssuedToPatron($borrowernumber, $biblionumber)
3803 Return 1 if any record item is issued to patron, otherwise return 0
3805 =cut
3807 sub CheckIfIssuedToPatron {
3808 my ($borrowernumber, $biblionumber) = @_;
3810 my $dbh = C4::Context->dbh;
3811 my $query = q|
3812 SELECT COUNT(*) FROM issues
3813 LEFT JOIN items ON items.itemnumber = issues.itemnumber
3814 WHERE items.biblionumber = ?
3815 AND issues.borrowernumber = ?
3817 my $is_issued = $dbh->selectrow_array($query, {}, $biblionumber, $borrowernumber );
3818 return 1 if $is_issued;
3819 return;
3822 =head2 IsItemIssued
3824 IsItemIssued( $itemnumber )
3826 Return 1 if the item is on loan, otherwise return 0
3828 =cut
3830 sub IsItemIssued {
3831 my $itemnumber = shift;
3832 my $dbh = C4::Context->dbh;
3833 my $sth = $dbh->prepare(q{
3834 SELECT COUNT(*)
3835 FROM issues
3836 WHERE itemnumber = ?
3838 $sth->execute($itemnumber);
3839 return $sth->fetchrow;
3842 =head2 GetAgeRestriction
3844 my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions, $borrower);
3845 my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions);
3847 if($daysToAgeRestriction <= 0) { #Borrower is allowed to access this material, as they are older or as old as the agerestriction }
3848 if($daysToAgeRestriction > 0) { #Borrower is this many days from meeting the agerestriction }
3850 @PARAM1 the koha.biblioitems.agerestriction value, like K18, PEGI 13, ...
3851 @PARAM2 a borrower-object with koha.borrowers.dateofbirth. (OPTIONAL)
3852 @RETURNS The age restriction age in years and the days to fulfill the age restriction for the given borrower.
3853 Negative days mean the borrower has gone past the age restriction age.
3855 =cut
3857 sub GetAgeRestriction {
3858 my ($record_restrictions, $borrower) = @_;
3859 my $markers = C4::Context->preference('AgeRestrictionMarker');
3861 # Split $record_restrictions to something like FSK 16 or PEGI 6
3862 my @values = split ' ', uc($record_restrictions);
3863 return unless @values;
3865 # Search first occurrence of one of the markers
3866 my @markers = split /\|/, uc($markers);
3867 return unless @markers;
3869 my $index = 0;
3870 my $restriction_year = 0;
3871 for my $value (@values) {
3872 $index++;
3873 for my $marker (@markers) {
3874 $marker =~ s/^\s+//; #remove leading spaces
3875 $marker =~ s/\s+$//; #remove trailing spaces
3876 if ( $marker eq $value ) {
3877 if ( $index <= $#values ) {
3878 $restriction_year += $values[$index];
3880 last;
3882 elsif ( $value =~ /^\Q$marker\E(\d+)$/ ) {
3884 # Perhaps it is something like "K16" (as in Finland)
3885 $restriction_year += $1;
3886 last;
3889 last if ( $restriction_year > 0 );
3892 #Check if the borrower is age restricted for this material and for how long.
3893 if ($restriction_year && $borrower) {
3894 if ( $borrower->{'dateofbirth'} ) {
3895 my @alloweddate = split /-/, $borrower->{'dateofbirth'};
3896 $alloweddate[0] += $restriction_year;
3898 #Prevent runime eror on leap year (invalid date)
3899 if ( ( $alloweddate[1] == 2 ) && ( $alloweddate[2] == 29 ) ) {
3900 $alloweddate[2] = 28;
3903 #Get how many days the borrower has to reach the age restriction
3904 my @Today = split /-/, DateTime->today->ymd();
3905 my $daysToAgeRestriction = Date_to_Days(@alloweddate) - Date_to_Days(@Today);
3906 #Negative days means the borrower went past the age restriction age
3907 return ($restriction_year, $daysToAgeRestriction);
3911 return ($restriction_year);
3915 =head2 GetPendingOnSiteCheckouts
3917 =cut
3919 sub GetPendingOnSiteCheckouts {
3920 my $dbh = C4::Context->dbh;
3921 return $dbh->selectall_arrayref(q|
3922 SELECT
3923 items.barcode,
3924 items.biblionumber,
3925 items.itemnumber,
3926 items.itemnotes,
3927 items.itemcallnumber,
3928 items.location,
3929 issues.date_due,
3930 issues.branchcode,
3931 issues.date_due < NOW() AS is_overdue,
3932 biblio.author,
3933 biblio.title,
3934 borrowers.firstname,
3935 borrowers.surname,
3936 borrowers.cardnumber,
3937 borrowers.borrowernumber
3938 FROM items
3939 LEFT JOIN issues ON items.itemnumber = issues.itemnumber
3940 LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
3941 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
3942 WHERE issues.onsite_checkout = 1
3943 |, { Slice => {} } );
3946 sub GetTopIssues {
3947 my ($params) = @_;
3949 my ($count, $branch, $itemtype, $ccode, $newness)
3950 = @$params{qw(count branch itemtype ccode newness)};
3952 my $dbh = C4::Context->dbh;
3953 my $query = q{
3954 SELECT b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
3955 bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
3956 i.ccode, SUM(i.issues) AS count
3957 FROM biblio b
3958 LEFT JOIN items i ON (i.biblionumber = b.biblionumber)
3959 LEFT JOIN biblioitems bi ON (bi.biblionumber = b.biblionumber)
3962 my (@where_strs, @where_args);
3964 if ($branch) {
3965 push @where_strs, 'i.homebranch = ?';
3966 push @where_args, $branch;
3968 if ($itemtype) {
3969 if (C4::Context->preference('item-level_itypes')){
3970 push @where_strs, 'i.itype = ?';
3971 push @where_args, $itemtype;
3972 } else {
3973 push @where_strs, 'bi.itemtype = ?';
3974 push @where_args, $itemtype;
3977 if ($ccode) {
3978 push @where_strs, 'i.ccode = ?';
3979 push @where_args, $ccode;
3981 if ($newness) {
3982 push @where_strs, 'TO_DAYS(NOW()) - TO_DAYS(b.datecreated) <= ?';
3983 push @where_args, $newness;
3986 if (@where_strs) {
3987 $query .= 'WHERE ' . join(' AND ', @where_strs);
3990 $query .= q{
3991 GROUP BY b.biblionumber
3992 HAVING count > 0
3993 ORDER BY count DESC
3996 $count = int($count);
3997 if ($count > 0) {
3998 $query .= "LIMIT $count";
4001 my $rows = $dbh->selectall_arrayref($query, { Slice => {} }, @where_args);
4003 return @$rows;
4006 sub _CalculateAndUpdateFine {
4007 my ($params) = @_;
4009 my $borrower = $params->{borrower};
4010 my $item = $params->{item};
4011 my $issue = $params->{issue};
4012 my $return_date = $params->{return_date};
4014 unless ($borrower) { carp "No borrower passed in!" && return; }
4015 unless ($item) { carp "No item passed in!" && return; }
4016 unless ($issue) { carp "No issue passed in!" && return; }
4018 my $datedue = dt_from_string( $issue->date_due );
4020 # we only need to calculate and change the fines if we want to do that on return
4021 # Should be on for hourly loans
4022 my $control = C4::Context->preference('CircControl');
4023 my $control_branchcode =
4024 ( $control eq 'ItemHomeLibrary' ) ? $item->{homebranch}
4025 : ( $control eq 'PatronLibrary' ) ? $borrower->{branchcode}
4026 : $issue->branchcode;
4028 my $date_returned = $return_date ? dt_from_string($return_date) : dt_from_string();
4030 my ( $amount, $type, $unitcounttotal ) =
4031 C4::Overdues::CalcFine( $item, $borrower->{categorycode}, $control_branchcode, $datedue, $date_returned );
4033 $type ||= q{};
4035 if ( C4::Context->preference('finesMode') eq 'production' ) {
4036 if ( $amount > 0 ) {
4037 C4::Overdues::UpdateFine({
4038 issue_id => $issue->issue_id,
4039 itemnumber => $issue->itemnumber,
4040 borrowernumber => $issue->borrowernumber,
4041 amount => $amount,
4042 type => $type,
4043 due => output_pref($datedue),
4046 elsif ($return_date) {
4048 # Backdated returns may have fines that shouldn't exist,
4049 # so in this case, we need to drop those fines to 0
4051 C4::Overdues::UpdateFine({
4052 issue_id => $issue->issue_id,
4053 itemnumber => $issue->itemnumber,
4054 borrowernumber => $issue->borrowernumber,
4055 amount => 0,
4056 type => $type,
4057 due => output_pref($datedue),
4065 __END__
4067 =head1 AUTHOR
4069 Koha Development Team <http://koha-community.org/>
4071 =cut