Bug 17781 - Improper branchcode set during renewal
[koha.git] / C4 / Circulation.pm
blobf1c9c2526d610602072b2d168cc912a7c94faea2
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::DateUtils;
45 use Koha::Calendar;
46 use Koha::Checkouts;
47 use Koha::IssuingRules;
48 use Koha::Items;
49 use Koha::Patrons;
50 use Koha::Patron::Debarments;
51 use Koha::Database;
52 use Koha::Libraries;
53 use Koha::Holds;
54 use Koha::RefundLostItemFeeRule;
55 use Koha::RefundLostItemFeeRules;
56 use Carp;
57 use List::MoreUtils qw( uniq );
58 use Scalar::Util qw( looks_like_number );
59 use Date::Calc qw(
60 Today
61 Today_and_Now
62 Add_Delta_YM
63 Add_Delta_DHMS
64 Date_to_Days
65 Day_of_Week
66 Add_Delta_Days
68 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
70 BEGIN {
71 require Exporter;
72 @ISA = qw(Exporter);
74 # FIXME subs that should probably be elsewhere
75 push @EXPORT, qw(
76 &barcodedecode
77 &LostItem
78 &ReturnLostItem
79 &GetPendingOnSiteCheckouts
82 # subs to deal with issuing a book
83 push @EXPORT, qw(
84 &CanBookBeIssued
85 &CanBookBeRenewed
86 &AddIssue
87 &AddRenewal
88 &GetRenewCount
89 &GetSoonestRenewDate
90 &GetLatestAutoRenewDate
91 &GetItemIssue
92 &GetItemIssues
93 &GetIssuingCharges
94 &GetBranchBorrowerCircRule
95 &GetBranchItemRule
96 &GetBiblioIssues
97 &GetOpenIssue
98 &AnonymiseIssueHistory
99 &CheckIfIssuedToPatron
100 &IsItemIssued
101 GetTopIssues
104 # subs to deal with returns
105 push @EXPORT, qw(
106 &AddReturn
107 &MarkIssueReturned
110 # subs to deal with transfers
111 push @EXPORT, qw(
112 &transferbook
113 &GetTransfers
114 &GetTransfersFromTo
115 &updateWrongTransfer
116 &DeleteTransfer
117 &IsBranchTransferAllowed
118 &CreateBranchTransferLimit
119 &DeleteBranchTransferLimits
120 &TransferSlip
123 # subs to deal with offline circulation
124 push @EXPORT, qw(
125 &GetOfflineOperations
126 &GetOfflineOperation
127 &AddOfflineOperation
128 &DeleteOfflineOperation
129 &ProcessOfflineOperation
133 =head1 NAME
135 C4::Circulation - Koha circulation module
137 =head1 SYNOPSIS
139 use C4::Circulation;
141 =head1 DESCRIPTION
143 The functions in this module deal with circulation, issues, and
144 returns, as well as general information about the library.
145 Also deals with inventory.
147 =head1 FUNCTIONS
149 =head2 barcodedecode
151 $str = &barcodedecode($barcode, [$filter]);
153 Generic filter function for barcode string.
154 Called on every circ if the System Pref itemBarcodeInputFilter is set.
155 Will do some manipulation of the barcode for systems that deliver a barcode
156 to circulation.pl that differs from the barcode stored for the item.
157 For proper functioning of this filter, calling the function on the
158 correct barcode string (items.barcode) should return an unaltered barcode.
160 The optional $filter argument is to allow for testing or explicit
161 behavior that ignores the System Pref. Valid values are the same as the
162 System Pref options.
164 =cut
166 # FIXME -- the &decode fcn below should be wrapped into this one.
167 # FIXME -- these plugins should be moved out of Circulation.pm
169 sub barcodedecode {
170 my ($barcode, $filter) = @_;
171 my $branch = C4::Context::mybranch();
172 $filter = C4::Context->preference('itemBarcodeInputFilter') unless $filter;
173 $filter or return $barcode; # ensure filter is defined, else return untouched barcode
174 if ($filter eq 'whitespace') {
175 $barcode =~ s/\s//g;
176 } elsif ($filter eq 'cuecat') {
177 chomp($barcode);
178 my @fields = split( /\./, $barcode );
179 my @results = map( decode($_), @fields[ 1 .. $#fields ] );
180 ($#results == 2) and return $results[2];
181 } elsif ($filter eq 'T-prefix') {
182 if ($barcode =~ /^[Tt](\d)/) {
183 (defined($1) and $1 eq '0') and return $barcode;
184 $barcode = substr($barcode, 2) + 0; # FIXME: probably should be substr($barcode, 1)
186 return sprintf("T%07d", $barcode);
187 # FIXME: $barcode could be "T1", causing warning: substr outside of string
188 # Why drop the nonzero digit after the T?
189 # Why pass non-digits (or empty string) to "T%07d"?
190 } elsif ($filter eq 'libsuite8') {
191 unless($barcode =~ m/^($branch)-/i){ #if barcode starts with branch code its in Koha style. Skip it.
192 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
193 $barcode =~ s/^[0]*(\d+)$/$branch-b-$1/i;
194 }else{
195 $barcode =~ s/^(\D+)[0]*(\d+)$/$branch-$1-$2/i;
198 } elsif ($filter eq 'EAN13') {
199 my $ean = CheckDigits('ean');
200 if ( $ean->is_valid($barcode) ) {
201 #$barcode = sprintf('%013d',$barcode); # this doesn't work on 32-bit systems
202 $barcode = '0' x ( 13 - length($barcode) ) . $barcode;
203 } else {
204 warn "# [$barcode] not valid EAN-13/UPC-A\n";
207 return $barcode; # return barcode, modified or not
210 =head2 decode
212 $str = &decode($chunk);
214 Decodes a segment of a string emitted by a CueCat barcode scanner and
215 returns it.
217 FIXME: Should be replaced with Barcode::Cuecat from CPAN
218 or Javascript based decoding on the client side.
220 =cut
222 sub decode {
223 my ($encoded) = @_;
224 my $seq =
225 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-';
226 my @s = map { index( $seq, $_ ); } split( //, $encoded );
227 my $l = ( $#s + 1 ) % 4;
228 if ($l) {
229 if ( $l == 1 ) {
230 # warn "Error: Cuecat decode parsing failed!";
231 return;
233 $l = 4 - $l;
234 $#s += $l;
236 my $r = '';
237 while ( $#s >= 0 ) {
238 my $n = ( ( $s[0] << 6 | $s[1] ) << 6 | $s[2] ) << 6 | $s[3];
239 $r .=
240 chr( ( $n >> 16 ) ^ 67 )
241 .chr( ( $n >> 8 & 255 ) ^ 67 )
242 .chr( ( $n & 255 ) ^ 67 );
243 @s = @s[ 4 .. $#s ];
245 $r = substr( $r, 0, length($r) - $l );
246 return $r;
249 =head2 transferbook
251 ($dotransfer, $messages, $iteminformation) = &transferbook($newbranch,
252 $barcode, $ignore_reserves);
254 Transfers an item to a new branch. If the item is currently on loan, it is automatically returned before the actual transfer.
256 C<$newbranch> is the code for the branch to which the item should be transferred.
258 C<$barcode> is the barcode of the item to be transferred.
260 If C<$ignore_reserves> is true, C<&transferbook> ignores reserves.
261 Otherwise, if an item is reserved, the transfer fails.
263 Returns three values:
265 =over
267 =item $dotransfer
269 is true if the transfer was successful.
271 =item $messages
273 is a reference-to-hash which may have any of the following keys:
275 =over
277 =item C<BadBarcode>
279 There is no item in the catalog with the given barcode. The value is C<$barcode>.
281 =item C<IsPermanent>
283 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.
285 =item C<DestinationEqualsHolding>
287 The item is already at the branch to which it is being transferred. The transfer is nonetheless considered to have failed. The value should be ignored.
289 =item C<WasReturned>
291 The item was on loan, and C<&transferbook> automatically returned it before transferring it. The value is the borrower number of the patron who had the item.
293 =item C<ResFound>
295 The item was reserved. The value is a reference-to-hash whose keys are fields from the reserves table of the Koha database, and C<biblioitemnumber>. It also has the key C<ResFound>, whose value is either C<Waiting> or C<Reserved>.
297 =item C<WasTransferred>
299 The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored.
301 =back
303 =back
305 =cut
307 sub transferbook {
308 my ( $tbr, $barcode, $ignoreRs ) = @_;
309 my $messages;
310 my $dotransfer = 1;
311 my $itemnumber = GetItemnumberFromBarcode( $barcode );
312 my $issue = GetItemIssue($itemnumber);
313 my $biblio = GetBiblioFromItemNumber($itemnumber);
315 # bad barcode..
316 if ( not $itemnumber ) {
317 $messages->{'BadBarcode'} = $barcode;
318 $dotransfer = 0;
321 # get branches of book...
322 my $hbr = $biblio->{'homebranch'};
323 my $fbr = $biblio->{'holdingbranch'};
325 # if using Branch Transfer Limits
326 if ( C4::Context->preference("UseBranchTransferLimits") == 1 ) {
327 if ( C4::Context->preference("item-level_itypes") && C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ) {
328 if ( ! IsBranchTransferAllowed( $tbr, $fbr, $biblio->{'itype'} ) ) {
329 $messages->{'NotAllowed'} = $tbr . "::" . $biblio->{'itype'};
330 $dotransfer = 0;
332 } elsif ( ! IsBranchTransferAllowed( $tbr, $fbr, $biblio->{ C4::Context->preference("BranchTransferLimitsType") } ) ) {
333 $messages->{'NotAllowed'} = $tbr . "::" . $biblio->{ C4::Context->preference("BranchTransferLimitsType") };
334 $dotransfer = 0;
338 # if is permanent...
339 # FIXME Is this still used by someone?
340 # See other FIXME in AddReturn
341 my $library = Koha::Libraries->find($hbr);
342 if ( $library and $library->get_categories->search({'me.categorycode' => 'PE'})->count ) {
343 $messages->{'IsPermanent'} = $hbr;
344 $dotransfer = 0;
347 # can't transfer book if is already there....
348 if ( $fbr eq $tbr ) {
349 $messages->{'DestinationEqualsHolding'} = 1;
350 $dotransfer = 0;
353 # check if it is still issued to someone, return it...
354 if ($issue->{borrowernumber}) {
355 AddReturn( $barcode, $fbr );
356 $messages->{'WasReturned'} = $issue->{borrowernumber};
359 # find reserves.....
360 # That'll save a database query.
361 my ( $resfound, $resrec, undef ) =
362 CheckReserves( $itemnumber );
363 if ( $resfound and not $ignoreRs ) {
364 $resrec->{'ResFound'} = $resfound;
366 # $messages->{'ResFound'} = $resrec;
367 $dotransfer = 1;
370 #actually do the transfer....
371 if ($dotransfer) {
372 ModItemTransfer( $itemnumber, $fbr, $tbr );
374 # don't need to update MARC anymore, we do it in batch now
375 $messages->{'WasTransfered'} = 1;
378 ModDateLastSeen( $itemnumber );
379 return ( $dotransfer, $messages, $biblio );
383 sub TooMany {
384 my $borrower = shift;
385 my $biblionumber = shift;
386 my $item = shift;
387 my $params = shift;
388 my $onsite_checkout = $params->{onsite_checkout} || 0;
389 my $switch_onsite_checkout = $params->{switch_onsite_checkout} || 0;
390 my $cat_borrower = $borrower->{'categorycode'};
391 my $dbh = C4::Context->dbh;
392 my $branch;
393 # Get which branchcode we need
394 $branch = _GetCircControlBranch($item,$borrower);
395 my $type = (C4::Context->preference('item-level_itypes'))
396 ? $item->{'itype'} # item-level
397 : $item->{'itemtype'}; # biblio-level
399 # given branch, patron category, and item type, determine
400 # applicable issuing rule
401 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
402 { categorycode => $cat_borrower,
403 itemtype => $type,
404 branchcode => $branch
409 # if a rule is found and has a loan limit set, count
410 # how many loans the patron already has that meet that
411 # rule
412 if (defined($issuing_rule) and defined($issuing_rule->maxissueqty)) {
413 my @bind_params;
414 my $count_query = q|
415 SELECT COUNT(*) AS total, COALESCE(SUM(onsite_checkout), 0) AS onsite_checkouts
416 FROM issues
417 JOIN items USING (itemnumber)
420 my $rule_itemtype = $issuing_rule->itemtype;
421 if ($rule_itemtype eq "*") {
422 # matching rule has the default item type, so count only
423 # those existing loans that don't fall under a more
424 # specific rule
425 if (C4::Context->preference('item-level_itypes')) {
426 $count_query .= " WHERE items.itype NOT IN (
427 SELECT itemtype FROM issuingrules
428 WHERE branchcode = ?
429 AND (categorycode = ? OR categorycode = ?)
430 AND itemtype <> '*'
431 ) ";
432 } else {
433 $count_query .= " JOIN biblioitems USING (biblionumber)
434 WHERE biblioitems.itemtype NOT IN (
435 SELECT itemtype FROM issuingrules
436 WHERE branchcode = ?
437 AND (categorycode = ? OR categorycode = ?)
438 AND itemtype <> '*'
439 ) ";
441 push @bind_params, $issuing_rule->branchcode;
442 push @bind_params, $issuing_rule->categorycode;
443 push @bind_params, $cat_borrower;
444 } else {
445 # rule has specific item type, so count loans of that
446 # specific item type
447 if (C4::Context->preference('item-level_itypes')) {
448 $count_query .= " WHERE items.itype = ? ";
449 } else {
450 $count_query .= " JOIN biblioitems USING (biblionumber)
451 WHERE biblioitems.itemtype= ? ";
453 push @bind_params, $type;
456 $count_query .= " AND borrowernumber = ? ";
457 push @bind_params, $borrower->{'borrowernumber'};
458 my $rule_branch = $issuing_rule->branchcode;
459 if ($rule_branch ne "*") {
460 if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
461 $count_query .= " AND issues.branchcode = ? ";
462 push @bind_params, $branch;
463 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
464 ; # if branch is the patron's home branch, then count all loans by patron
465 } else {
466 $count_query .= " AND items.homebranch = ? ";
467 push @bind_params, $branch;
471 my ( $checkout_count, $onsite_checkout_count ) = $dbh->selectrow_array( $count_query, {}, @bind_params );
473 my $max_checkouts_allowed = $issuing_rule->maxissueqty;
474 my $max_onsite_checkouts_allowed = $issuing_rule->maxonsiteissueqty;
476 if ( $onsite_checkout ) {
477 if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed ) {
478 return {
479 reason => 'TOO_MANY_ONSITE_CHECKOUTS',
480 count => $onsite_checkout_count,
481 max_allowed => $max_onsite_checkouts_allowed,
485 if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) {
486 my $delta = $switch_onsite_checkout ? 1 : 0;
487 if ( $checkout_count >= $max_checkouts_allowed + $delta ) {
488 return {
489 reason => 'TOO_MANY_CHECKOUTS',
490 count => $checkout_count,
491 max_allowed => $max_checkouts_allowed,
494 } elsif ( not $onsite_checkout ) {
495 if ( $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed ) {
496 return {
497 reason => 'TOO_MANY_CHECKOUTS',
498 count => $checkout_count - $onsite_checkout_count,
499 max_allowed => $max_checkouts_allowed,
505 # Now count total loans against the limit for the branch
506 my $branch_borrower_circ_rule = GetBranchBorrowerCircRule($branch, $cat_borrower);
507 if (defined($branch_borrower_circ_rule->{maxissueqty})) {
508 my @bind_params = ();
509 my $branch_count_query = q|
510 SELECT COUNT(*) AS total, COALESCE(SUM(onsite_checkout), 0) AS onsite_checkouts
511 FROM issues
512 JOIN items USING (itemnumber)
513 WHERE borrowernumber = ?
515 push @bind_params, $borrower->{borrowernumber};
517 if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
518 $branch_count_query .= " AND issues.branchcode = ? ";
519 push @bind_params, $branch;
520 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
521 ; # if branch is the patron's home branch, then count all loans by patron
522 } else {
523 $branch_count_query .= " AND items.homebranch = ? ";
524 push @bind_params, $branch;
526 my ( $checkout_count, $onsite_checkout_count ) = $dbh->selectrow_array( $branch_count_query, {}, @bind_params );
527 my $max_checkouts_allowed = $branch_borrower_circ_rule->{maxissueqty};
528 my $max_onsite_checkouts_allowed = $branch_borrower_circ_rule->{maxonsiteissueqty};
530 if ( $onsite_checkout ) {
531 if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed ) {
532 return {
533 reason => 'TOO_MANY_ONSITE_CHECKOUTS',
534 count => $onsite_checkout_count,
535 max_allowed => $max_onsite_checkouts_allowed,
539 if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) {
540 my $delta = $switch_onsite_checkout ? 1 : 0;
541 if ( $checkout_count >= $max_checkouts_allowed + $delta ) {
542 return {
543 reason => 'TOO_MANY_CHECKOUTS',
544 count => $checkout_count,
545 max_allowed => $max_checkouts_allowed,
548 } elsif ( not $onsite_checkout ) {
549 if ( $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed ) {
550 return {
551 reason => 'TOO_MANY_CHECKOUTS',
552 count => $checkout_count - $onsite_checkout_count,
553 max_allowed => $max_checkouts_allowed,
559 # OK, the patron can issue !!!
560 return;
563 =head2 CanBookBeIssued
565 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $borrower,
566 $barcode, $duedate, $inprocess, $ignore_reserves, $params );
568 Check if a book can be issued.
570 C<$issuingimpossible> and C<$needsconfirmation> are some hashref.
572 =over 4
574 =item C<$borrower> hash with borrower informations (from GetMember)
576 =item C<$barcode> is the bar code of the book being issued.
578 =item C<$duedates> is a DateTime object.
580 =item C<$inprocess> boolean switch
582 =item C<$ignore_reserves> boolean switch
584 =item C<$params> Hashref of additional parameters
586 Available keys:
587 override_high_holds - Ignore high holds
588 onsite_checkout - Checkout is an onsite checkout that will not leave the library
590 =back
592 Returns :
594 =over 4
596 =item C<$issuingimpossible> a reference to a hash. It contains reasons why issuing is impossible.
597 Possible values are :
599 =back
601 =head3 INVALID_DATE
603 sticky due date is invalid
605 =head3 GNA
607 borrower gone with no address
609 =head3 CARD_LOST
611 borrower declared it's card lost
613 =head3 DEBARRED
615 borrower debarred
617 =head3 UNKNOWN_BARCODE
619 barcode unknown
621 =head3 NOT_FOR_LOAN
623 item is not for loan
625 =head3 WTHDRAWN
627 item withdrawn.
629 =head3 RESTRICTED
631 item is restricted (set by ??)
633 C<$needsconfirmation> a reference to a hash. It contains reasons why the loan
634 could be prevented, but ones that can be overriden by the operator.
636 Possible values are :
638 =head3 DEBT
640 borrower has debts.
642 =head3 RENEW_ISSUE
644 renewing, not issuing
646 =head3 ISSUED_TO_ANOTHER
648 issued to someone else.
650 =head3 RESERVED
652 reserved for someone else.
654 =head3 INVALID_DATE
656 sticky due date is invalid or due date in the past
658 =head3 TOO_MANY
660 if the borrower borrows to much things
662 =cut
664 sub CanBookBeIssued {
665 my ( $borrower, $barcode, $duedate, $inprocess, $ignore_reserves, $params ) = @_;
666 my %needsconfirmation; # filled with problems that needs confirmations
667 my %issuingimpossible; # filled with problems that causes the issue to be IMPOSSIBLE
668 my %alerts; # filled with messages that shouldn't stop issuing, but the librarian should be aware of.
669 my %messages; # filled with information messages that should be displayed.
671 my $onsite_checkout = $params->{onsite_checkout} || 0;
672 my $override_high_holds = $params->{override_high_holds} || 0;
674 my $item = GetItem(GetItemnumberFromBarcode( $barcode ));
675 my $issue = GetItemIssue($item->{itemnumber});
676 my $biblioitem = GetBiblioItemData($item->{biblioitemnumber});
677 $item->{'itemtype'}=$item->{'itype'};
678 my $dbh = C4::Context->dbh;
680 # MANDATORY CHECKS - unless item exists, nothing else matters
681 unless ( $item->{barcode} ) {
682 $issuingimpossible{UNKNOWN_BARCODE} = 1;
684 return ( \%issuingimpossible, \%needsconfirmation ) if %issuingimpossible;
687 # DUE DATE is OK ? -- should already have checked.
689 if ($duedate && ref $duedate ne 'DateTime') {
690 $duedate = dt_from_string($duedate);
692 my $now = DateTime->now( time_zone => C4::Context->tz() );
693 unless ( $duedate ) {
694 my $issuedate = $now->clone();
696 my $branch = _GetCircControlBranch($item,$borrower);
697 my $itype = ( C4::Context->preference('item-level_itypes') ) ? $item->{'itype'} : $biblioitem->{'itemtype'};
698 $duedate = CalcDateDue( $issuedate, $itype, $branch, $borrower );
700 # Offline circ calls AddIssue directly, doesn't run through here
701 # So issuingimpossible should be ok.
703 if ($duedate) {
704 my $today = $now->clone();
705 $today->truncate( to => 'minute');
706 if (DateTime->compare($duedate,$today) == -1 ) { # duedate cannot be before now
707 $needsconfirmation{INVALID_DATE} = output_pref($duedate);
709 } else {
710 $issuingimpossible{INVALID_DATE} = output_pref($duedate);
714 # BORROWER STATUS
716 if ( $borrower->{'category_type'} eq 'X' && ( $item->{barcode} )) {
717 # stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1 .
718 &UpdateStats({
719 branch => C4::Context->userenv->{'branch'},
720 type => 'localuse',
721 itemnumber => $item->{'itemnumber'},
722 itemtype => $item->{'itype'},
723 borrowernumber => $borrower->{'borrowernumber'},
724 ccode => $item->{'ccode'}}
726 ModDateLastSeen( $item->{'itemnumber'} );
727 return( { STATS => 1 }, {});
730 my $flags = C4::Members::patronflags( $borrower );
731 if ( ref $flags ) {
732 if ( $flags->{GNA} ) {
733 $issuingimpossible{GNA} = 1;
735 if ( $flags->{'LOST'} ) {
736 $issuingimpossible{CARD_LOST} = 1;
738 if ( $flags->{'DBARRED'} ) {
739 $issuingimpossible{DEBARRED} = 1;
742 if ( !defined $borrower->{dateexpiry} || $borrower->{'dateexpiry'} eq '0000-00-00') {
743 $issuingimpossible{EXPIRED} = 1;
744 } else {
745 my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'sql', 'floating' );
746 $expiry_dt->truncate( to => 'day');
747 my $today = $now->clone()->truncate(to => 'day');
748 $today->set_time_zone( 'floating' );
749 if ( DateTime->compare($today, $expiry_dt) == 1 ) {
750 $issuingimpossible{EXPIRED} = 1;
755 # BORROWER STATUS
758 # DEBTS
759 my ($balance, $non_issue_charges, $other_charges) =
760 C4::Members::GetMemberAccountBalance( $borrower->{'borrowernumber'} );
762 my $amountlimit = C4::Context->preference("noissuescharge");
763 my $allowfineoverride = C4::Context->preference("AllowFineOverride");
764 my $allfinesneedoverride = C4::Context->preference("AllFinesNeedOverride");
766 # Check the debt of this patrons guarantees
767 my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
768 $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
769 if ( defined $no_issues_charge_guarantees ) {
770 my $p = Koha::Patrons->find( $borrower->{borrowernumber} );
771 my @guarantees = $p->guarantees();
772 my $guarantees_non_issues_charges;
773 foreach my $g ( @guarantees ) {
774 my ( $b, $n, $o ) = C4::Members::GetMemberAccountBalance( $g->id );
775 $guarantees_non_issues_charges += $n;
778 if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && !$allowfineoverride) {
779 $issuingimpossible{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
780 } elsif ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && $allowfineoverride) {
781 $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
782 } elsif ( $allfinesneedoverride && $guarantees_non_issues_charges > 0 && $guarantees_non_issues_charges <= $no_issues_charge_guarantees && !$inprocess ) {
783 $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
787 if ( C4::Context->preference("IssuingInProcess") ) {
788 if ( $non_issue_charges > $amountlimit && !$inprocess && !$allowfineoverride) {
789 $issuingimpossible{DEBT} = sprintf( "%.2f", $non_issue_charges );
790 } elsif ( $non_issue_charges > $amountlimit && !$inprocess && $allowfineoverride) {
791 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
792 } elsif ( $allfinesneedoverride && $non_issue_charges > 0 && $non_issue_charges <= $amountlimit && !$inprocess ) {
793 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
796 else {
797 if ( $non_issue_charges > $amountlimit && $allowfineoverride ) {
798 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
799 } elsif ( $non_issue_charges > $amountlimit && !$allowfineoverride) {
800 $issuingimpossible{DEBT} = sprintf( "%.2f", $non_issue_charges );
801 } elsif ( $non_issue_charges > 0 && $allfinesneedoverride ) {
802 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
806 if ($balance > 0 && $other_charges > 0) {
807 $alerts{OTHER_CHARGES} = sprintf( "%.2f", $other_charges );
810 my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
811 if ( my $debarred_date = $patron->is_debarred ) {
812 # patron has accrued fine days or has a restriction. $count is a date
813 if ($debarred_date eq '9999-12-31') {
814 $issuingimpossible{USERBLOCKEDNOENDDATE} = $debarred_date;
816 else {
817 $issuingimpossible{USERBLOCKEDWITHENDDATE} = $debarred_date;
819 } elsif ( my $num_overdues = $patron->has_overdues ) {
820 ## patron has outstanding overdue loans
821 if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){
822 $issuingimpossible{USERBLOCKEDOVERDUE} = $num_overdues;
824 elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){
825 $needsconfirmation{USERBLOCKEDOVERDUE} = $num_overdues;
829 # JB34 CHECKS IF BORROWERS DON'T HAVE ISSUE TOO MANY BOOKS
831 my $switch_onsite_checkout =
832 C4::Context->preference('SwitchOnSiteCheckouts')
833 and $issue->{onsite_checkout}
834 and $issue
835 and $issue->{borrowernumber} == $borrower->{'borrowernumber'} ? 1 : 0;
836 my $toomany = TooMany( $borrower, $item->{biblionumber}, $item, { onsite_checkout => $onsite_checkout, switch_onsite_checkout => $switch_onsite_checkout, } );
837 # if TooMany max_allowed returns 0 the user doesn't have permission to check out this book
838 if ( $toomany ) {
839 if ( $toomany->{max_allowed} == 0 ) {
840 $needsconfirmation{PATRON_CANT} = 1;
842 if ( C4::Context->preference("AllowTooManyOverride") ) {
843 $needsconfirmation{TOO_MANY} = $toomany->{reason};
844 $needsconfirmation{current_loan_count} = $toomany->{count};
845 $needsconfirmation{max_loans_allowed} = $toomany->{max_allowed};
846 } else {
847 $issuingimpossible{TOO_MANY} = $toomany->{reason};
848 $issuingimpossible{current_loan_count} = $toomany->{count};
849 $issuingimpossible{max_loans_allowed} = $toomany->{max_allowed};
854 # CHECKPREVCHECKOUT: CHECK IF ITEM HAS EVER BEEN LENT TO PATRON
856 $patron = Koha::Patrons->find($borrower->{borrowernumber});
857 my $wants_check = $patron->wants_check_for_previous_checkout;
858 $needsconfirmation{PREVISSUE} = 1
859 if ($wants_check and $patron->do_check_for_previous_checkout($item));
862 # ITEM CHECKING
864 if ( $item->{'notforloan'} )
866 if(!C4::Context->preference("AllowNotForLoanOverride")){
867 $issuingimpossible{NOT_FOR_LOAN} = 1;
868 $issuingimpossible{item_notforloan} = $item->{'notforloan'};
869 }else{
870 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
871 $needsconfirmation{item_notforloan} = $item->{'notforloan'};
874 else {
875 # we have to check itemtypes.notforloan also
876 if (C4::Context->preference('item-level_itypes')){
877 # this should probably be a subroutine
878 my $sth = $dbh->prepare("SELECT notforloan FROM itemtypes WHERE itemtype = ?");
879 $sth->execute($item->{'itemtype'});
880 my $notforloan=$sth->fetchrow_hashref();
881 if ($notforloan->{'notforloan'}) {
882 if (!C4::Context->preference("AllowNotForLoanOverride")) {
883 $issuingimpossible{NOT_FOR_LOAN} = 1;
884 $issuingimpossible{itemtype_notforloan} = $item->{'itype'};
885 } else {
886 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
887 $needsconfirmation{itemtype_notforloan} = $item->{'itype'};
891 elsif ($biblioitem->{'notforloan'} == 1){
892 if (!C4::Context->preference("AllowNotForLoanOverride")) {
893 $issuingimpossible{NOT_FOR_LOAN} = 1;
894 $issuingimpossible{itemtype_notforloan} = $biblioitem->{'itemtype'};
895 } else {
896 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
897 $needsconfirmation{itemtype_notforloan} = $biblioitem->{'itemtype'};
901 if ( $item->{'withdrawn'} && $item->{'withdrawn'} > 0 )
903 $issuingimpossible{WTHDRAWN} = 1;
905 if ( $item->{'restricted'}
906 && $item->{'restricted'} == 1 )
908 $issuingimpossible{RESTRICTED} = 1;
910 if ( $item->{'itemlost'} && C4::Context->preference("IssueLostItem") ne 'nothing' ) {
911 my $av = Koha::AuthorisedValues->search({ category => 'LOST', authorised_value => $item->{itemlost} });
912 my $code = $av->count ? $av->next->lib : '';
913 $needsconfirmation{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'confirm' );
914 $alerts{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'alert' );
916 if ( C4::Context->preference("IndependentBranches") ) {
917 my $userenv = C4::Context->userenv;
918 unless ( C4::Context->IsSuperLibrarian() ) {
919 if ( $item->{C4::Context->preference("HomeOrHoldingBranch")} ne $userenv->{branch} ){
920 $issuingimpossible{ITEMNOTSAMEBRANCH} = 1;
921 $issuingimpossible{'itemhomebranch'} = $item->{C4::Context->preference("HomeOrHoldingBranch")};
923 $needsconfirmation{BORRNOTSAMEBRANCH} = $borrower->{'branchcode'}
924 if ( $borrower->{'branchcode'} ne $userenv->{branch} );
928 # CHECK IF THERE IS RENTAL CHARGES. RENTAL MUST BE CONFIRMED BY THE BORROWER
930 my $rentalConfirmation = C4::Context->preference("RentalFeesCheckoutConfirmation");
932 if ( $rentalConfirmation ){
933 my ($rentalCharge) = GetIssuingCharges( $item->{'itemnumber'}, $borrower->{'borrowernumber'} );
934 if ( $rentalCharge > 0 ){
935 $rentalCharge = sprintf("%.02f", $rentalCharge);
936 $needsconfirmation{RENTALCHARGE} = $rentalCharge;
941 # CHECK IF BOOK ALREADY ISSUED TO THIS BORROWER
943 if ( $issue->{borrowernumber} && $issue->{borrowernumber} eq $borrower->{'borrowernumber'} ){
945 # Already issued to current borrower.
946 # If it is an on-site checkout if it can be switched to a normal checkout
947 # or ask whether the loan should be renewed
949 if ( $issue->{onsite_checkout}
950 and C4::Context->preference('SwitchOnSiteCheckouts') ) {
951 $messages{ONSITE_CHECKOUT_WILL_BE_SWITCHED} = 1;
952 } else {
953 my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed(
954 $borrower->{'borrowernumber'},
955 $item->{'itemnumber'},
957 if ( $CanBookBeRenewed == 0 ) { # no more renewals allowed
958 if ( $renewerror eq 'onsite_checkout' ) {
959 $issuingimpossible{NO_RENEWAL_FOR_ONSITE_CHECKOUTS} = 1;
961 else {
962 $issuingimpossible{NO_MORE_RENEWALS} = 1;
965 else {
966 $needsconfirmation{RENEW_ISSUE} = 1;
970 elsif ($issue->{borrowernumber}) {
972 # issued to someone else
973 my $currborinfo = C4::Members::GetMember( borrowernumber => $issue->{borrowernumber} );
976 my ( $can_be_returned, $message ) = CanBookBeReturned( $item, C4::Context->userenv->{branch} );
978 unless ( $can_be_returned ) {
979 $issuingimpossible{RETURN_IMPOSSIBLE} = 1;
980 $issuingimpossible{branch_to_return} = $message;
981 } else {
982 $needsconfirmation{ISSUED_TO_ANOTHER} = 1;
983 $needsconfirmation{issued_firstname} = $currborinfo->{'firstname'};
984 $needsconfirmation{issued_surname} = $currborinfo->{'surname'};
985 $needsconfirmation{issued_cardnumber} = $currborinfo->{'cardnumber'};
986 $needsconfirmation{issued_borrowernumber} = $currborinfo->{'borrowernumber'};
990 unless ( $ignore_reserves ) {
991 # See if the item is on reserve.
992 my ( $restype, $res ) = C4::Reserves::CheckReserves( $item->{'itemnumber'} );
993 if ($restype) {
994 my $resbor = $res->{'borrowernumber'};
995 if ( $resbor ne $borrower->{'borrowernumber'} ) {
996 my ( $resborrower ) = C4::Members::GetMember( borrowernumber => $resbor );
997 if ( $restype eq "Waiting" )
999 # The item is on reserve and waiting, but has been
1000 # reserved by some other patron.
1001 $needsconfirmation{RESERVE_WAITING} = 1;
1002 $needsconfirmation{'resfirstname'} = $resborrower->{'firstname'};
1003 $needsconfirmation{'ressurname'} = $resborrower->{'surname'};
1004 $needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'};
1005 $needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'};
1006 $needsconfirmation{'resbranchcode'} = $res->{branchcode};
1007 $needsconfirmation{'reswaitingdate'} = $res->{'waitingdate'};
1009 elsif ( $restype eq "Reserved" ) {
1010 # The item is on reserve for someone else.
1011 $needsconfirmation{RESERVED} = 1;
1012 $needsconfirmation{'resfirstname'} = $resborrower->{'firstname'};
1013 $needsconfirmation{'ressurname'} = $resborrower->{'surname'};
1014 $needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'};
1015 $needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'};
1016 $needsconfirmation{'resbranchcode'} = $res->{branchcode};
1017 $needsconfirmation{'resreservedate'} = $res->{'reservedate'};
1023 ## CHECK AGE RESTRICTION
1024 my $agerestriction = $biblioitem->{'agerestriction'};
1025 my ($restriction_age, $daysToAgeRestriction) = GetAgeRestriction( $agerestriction, $borrower );
1026 if ( $daysToAgeRestriction && $daysToAgeRestriction > 0 ) {
1027 if ( C4::Context->preference('AgeRestrictionOverride') ) {
1028 $needsconfirmation{AGE_RESTRICTION} = "$agerestriction";
1030 else {
1031 $issuingimpossible{AGE_RESTRICTION} = "$agerestriction";
1035 ## check for high holds decreasing loan period
1036 if ( C4::Context->preference('decreaseLoanHighHolds') ) {
1037 my $check = checkHighHolds( $item, $borrower );
1039 if ( $check->{exceeded} ) {
1040 if ($override_high_holds) {
1041 $alerts{HIGHHOLDS} = {
1042 num_holds => $check->{outstanding},
1043 duration => $check->{duration},
1044 returndate => output_pref( $check->{due_date} ),
1047 else {
1048 $needsconfirmation{HIGHHOLDS} = {
1049 num_holds => $check->{outstanding},
1050 duration => $check->{duration},
1051 returndate => output_pref( $check->{due_date} ),
1057 if (
1058 !C4::Context->preference('AllowMultipleIssuesOnABiblio') &&
1059 # don't do the multiple loans per bib check if we've
1060 # already determined that we've got a loan on the same item
1061 !$issuingimpossible{NO_MORE_RENEWALS} &&
1062 !$needsconfirmation{RENEW_ISSUE}
1064 # Check if borrower has already issued an item from the same biblio
1065 # Only if it's not a subscription
1066 my $biblionumber = $item->{biblionumber};
1067 require C4::Serials;
1068 my $is_a_subscription = C4::Serials::CountSubscriptionFromBiblionumber($biblionumber);
1069 unless ($is_a_subscription) {
1070 my $checkouts = Koha::Checkouts->search(
1072 borrowernumber => $borrower->{borrowernumber},
1073 biblionumber => $biblionumber,
1076 join => 'item',
1079 # if we get here, we don't already have a loan on this item,
1080 # so if there are any loans on this bib, ask for confirmation
1081 if ( $checkouts->count ) {
1082 $needsconfirmation{BIBLIO_ALREADY_ISSUED} = 1;
1087 return ( \%issuingimpossible, \%needsconfirmation, \%alerts, \%messages, );
1090 =head2 CanBookBeReturned
1092 ($returnallowed, $message) = CanBookBeReturned($item, $branch)
1094 Check whether the item can be returned to the provided branch
1096 =over 4
1098 =item C<$item> is a hash of item information as returned from GetItem
1100 =item C<$branch> is the branchcode where the return is taking place
1102 =back
1104 Returns:
1106 =over 4
1108 =item C<$returnallowed> is 0 or 1, corresponding to whether the return is allowed (1) or not (0)
1110 =item C<$message> is the branchcode where the item SHOULD be returned, if the return is not allowed
1112 =back
1114 =cut
1116 sub CanBookBeReturned {
1117 my ($item, $branch) = @_;
1118 my $allowreturntobranch = C4::Context->preference("AllowReturnToBranch") || 'anywhere';
1120 # assume return is allowed to start
1121 my $allowed = 1;
1122 my $message;
1124 # identify all cases where return is forbidden
1125 if ($allowreturntobranch eq 'homebranch' && $branch ne $item->{'homebranch'}) {
1126 $allowed = 0;
1127 $message = $item->{'homebranch'};
1128 } elsif ($allowreturntobranch eq 'holdingbranch' && $branch ne $item->{'holdingbranch'}) {
1129 $allowed = 0;
1130 $message = $item->{'holdingbranch'};
1131 } elsif ($allowreturntobranch eq 'homeorholdingbranch' && $branch ne $item->{'homebranch'} && $branch ne $item->{'holdingbranch'}) {
1132 $allowed = 0;
1133 $message = $item->{'homebranch'}; # FIXME: choice of homebranch is arbitrary
1136 return ($allowed, $message);
1139 =head2 CheckHighHolds
1141 used when syspref decreaseLoanHighHolds is active. Returns 1 or 0 to define whether the minimum value held in
1142 decreaseLoanHighHoldsValue is exceeded, the total number of outstanding holds, the number of days the loan
1143 has been decreased to (held in syspref decreaseLoanHighHoldsValue), and the new due date
1145 =cut
1147 sub checkHighHolds {
1148 my ( $item, $borrower ) = @_;
1149 my $biblio = GetBiblioFromItemNumber( $item->{itemnumber} );
1150 my $branch = _GetCircControlBranch( $item, $borrower );
1152 my $return_data = {
1153 exceeded => 0,
1154 outstanding => 0,
1155 duration => 0,
1156 due_date => undef,
1159 my $holds = Koha::Holds->search( { biblionumber => $item->{'biblionumber'} } );
1161 if ( $holds->count() ) {
1162 $return_data->{outstanding} = $holds->count();
1164 my $decreaseLoanHighHoldsControl = C4::Context->preference('decreaseLoanHighHoldsControl');
1165 my $decreaseLoanHighHoldsValue = C4::Context->preference('decreaseLoanHighHoldsValue');
1166 my $decreaseLoanHighHoldsIgnoreStatuses = C4::Context->preference('decreaseLoanHighHoldsIgnoreStatuses');
1168 my @decreaseLoanHighHoldsIgnoreStatuses = split( /,/, $decreaseLoanHighHoldsIgnoreStatuses );
1170 if ( $decreaseLoanHighHoldsControl eq 'static' ) {
1172 # static means just more than a given number of holds on the record
1174 # If the number of holds is less than the threshold, we can stop here
1175 if ( $holds->count() < $decreaseLoanHighHoldsValue ) {
1176 return $return_data;
1179 elsif ( $decreaseLoanHighHoldsControl eq 'dynamic' ) {
1181 # dynamic means X more than the number of holdable items on the record
1183 # let's get the items
1184 my @items = $holds->next()->biblio()->items();
1186 # Remove any items with status defined to be ignored even if the would not make item unholdable
1187 foreach my $status (@decreaseLoanHighHoldsIgnoreStatuses) {
1188 @items = grep { !$_->$status } @items;
1191 # Remove any items that are not holdable for this patron
1192 @items = grep { CanItemBeReserved( $borrower->{borrowernumber}, $_->itemnumber ) eq 'OK' } @items;
1194 my $items_count = scalar @items;
1196 my $threshold = $items_count + $decreaseLoanHighHoldsValue;
1198 # If the number of holds is less than the count of items we have
1199 # plus the number of holds allowed above that count, we can stop here
1200 if ( $holds->count() <= $threshold ) {
1201 return $return_data;
1205 my $issuedate = DateTime->now( time_zone => C4::Context->tz() );
1207 my $calendar = Koha::Calendar->new( branchcode => $branch );
1209 my $itype =
1210 ( C4::Context->preference('item-level_itypes') )
1211 ? $biblio->{'itype'}
1212 : $biblio->{'itemtype'};
1214 my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branch, $borrower );
1216 my $decreaseLoanHighHoldsDuration = C4::Context->preference('decreaseLoanHighHoldsDuration');
1218 my $reduced_datedue = $calendar->addDate( $issuedate, $decreaseLoanHighHoldsDuration );
1220 if ( DateTime->compare( $reduced_datedue, $orig_due ) == -1 ) {
1221 $return_data->{exceeded} = 1;
1222 $return_data->{duration} = $decreaseLoanHighHoldsDuration;
1223 $return_data->{due_date} = $reduced_datedue;
1227 return $return_data;
1230 =head2 AddIssue
1232 &AddIssue($borrower, $barcode, [$datedue], [$cancelreserve], [$issuedate])
1234 Issue a book. Does no check, they are done in CanBookBeIssued. If we reach this sub, it means the user confirmed if needed.
1236 =over 4
1238 =item C<$borrower> is a hash with borrower informations (from GetMember).
1240 =item C<$barcode> is the barcode of the item being issued.
1242 =item C<$datedue> is a DateTime object for the max date of return, i.e. the date due (optional).
1243 Calculated if empty.
1245 =item C<$cancelreserve> is 1 to override and cancel any pending reserves for the item (optional).
1247 =item C<$issuedate> is the date to issue the item in iso (YYYY-MM-DD) format (optional).
1248 Defaults to today. Unlike C<$datedue>, NOT a DateTime object, unfortunately.
1250 AddIssue does the following things :
1252 - step 01: check that there is a borrowernumber & a barcode provided
1253 - check for RENEWAL (book issued & being issued to the same patron)
1254 - renewal YES = Calculate Charge & renew
1255 - renewal NO =
1256 * BOOK ACTUALLY ISSUED ? do a return if book is actually issued (but to someone else)
1257 * RESERVE PLACED ?
1258 - fill reserve if reserve to this patron
1259 - cancel reserve or not, otherwise
1260 * TRANSFERT PENDING ?
1261 - complete the transfert
1262 * ISSUE THE BOOK
1264 =back
1266 =cut
1268 sub AddIssue {
1269 my ( $borrower, $barcode, $datedue, $cancelreserve, $issuedate, $sipmode, $params ) = @_;
1271 my $onsite_checkout = $params && $params->{onsite_checkout} ? 1 : 0;
1272 my $switch_onsite_checkout = $params && $params->{switch_onsite_checkout};
1273 my $auto_renew = $params && $params->{auto_renew};
1274 my $dbh = C4::Context->dbh;
1275 my $barcodecheck = CheckValidBarcode($barcode);
1277 my $issue;
1279 if ( $datedue && ref $datedue ne 'DateTime' ) {
1280 $datedue = dt_from_string($datedue);
1283 # $issuedate defaults to today.
1284 if ( !defined $issuedate ) {
1285 $issuedate = DateTime->now( time_zone => C4::Context->tz() );
1287 else {
1288 if ( ref $issuedate ne 'DateTime' ) {
1289 $issuedate = dt_from_string($issuedate);
1294 # Stop here if the patron or barcode doesn't exist
1295 if ( $borrower && $barcode && $barcodecheck ) {
1296 # find which item we issue
1297 my $item = GetItem( '', $barcode )
1298 or return; # if we don't get an Item, abort.
1300 my $branch = _GetCircControlBranch( $item, $borrower );
1302 # get actual issuing if there is one
1303 my $actualissue = GetItemIssue( $item->{itemnumber} );
1305 # get biblioinformation for this item
1306 my $biblio = GetBiblioFromItemNumber( $item->{itemnumber} );
1308 # check if we just renew the issue.
1309 if ( $actualissue->{borrowernumber} eq $borrower->{'borrowernumber'}
1310 and not $switch_onsite_checkout ) {
1311 $datedue = AddRenewal(
1312 $borrower->{'borrowernumber'},
1313 $item->{'itemnumber'},
1314 $branch,
1315 $datedue,
1316 $issuedate, # here interpreted as the renewal date
1319 else {
1320 # it's NOT a renewal
1321 if ( $actualissue->{borrowernumber}
1322 and not $switch_onsite_checkout ) {
1323 # This book is currently on loan, but not to the person
1324 # who wants to borrow it now. mark it returned before issuing to the new borrower
1325 my ( $allowed, $message ) = CanBookBeReturned( $item, C4::Context->userenv->{branch} );
1326 return unless $allowed;
1327 AddReturn( $item->{'barcode'}, C4::Context->userenv->{'branch'} );
1330 MoveReserve( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $cancelreserve );
1332 # Starting process for transfer job (checking transfert and validate it if we have one)
1333 my ($datesent) = GetTransfers( $item->{'itemnumber'} );
1334 if ($datesent) {
1335 # updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....)
1336 my $sth = $dbh->prepare(
1337 "UPDATE branchtransfers
1338 SET datearrived = now(),
1339 tobranch = ?,
1340 comments = 'Forced branchtransfer'
1341 WHERE itemnumber= ? AND datearrived IS NULL"
1343 $sth->execute( C4::Context->userenv->{'branch'},
1344 $item->{'itemnumber'} );
1347 # If automatic renewal wasn't selected while issuing, set the value according to the issuing rule.
1348 unless ($auto_renew) {
1349 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
1350 { categorycode => $borrower->{categorycode},
1351 itemtype => $item->{itype},
1352 branchcode => $branch
1356 $auto_renew = $issuing_rule->auto_renew if $issuing_rule;
1359 # Record in the database the fact that the book was issued.
1360 unless ($datedue) {
1361 my $itype =
1362 ( C4::Context->preference('item-level_itypes') )
1363 ? $biblio->{'itype'}
1364 : $biblio->{'itemtype'};
1365 $datedue = CalcDateDue( $issuedate, $itype, $branch, $borrower );
1368 $datedue->truncate( to => 'minute' );
1370 $issue = Koha::Database->new()->schema()->resultset('Issue')->update_or_create(
1372 borrowernumber => $borrower->{'borrowernumber'},
1373 itemnumber => $item->{'itemnumber'},
1374 issuedate => $issuedate->strftime('%Y-%m-%d %H:%M:%S'),
1375 date_due => $datedue->strftime('%Y-%m-%d %H:%M:%S'),
1376 branchcode => C4::Context->userenv->{'branch'},
1377 onsite_checkout => $onsite_checkout,
1378 auto_renew => $auto_renew ? 1 : 0
1382 if ( C4::Context->preference('ReturnToShelvingCart') ) {
1383 # ReturnToShelvingCart is on, anything issued should be taken off the cart.
1384 CartToShelf( $item->{'itemnumber'} );
1386 $item->{'issues'}++;
1387 if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) {
1388 UpdateTotalIssues( $item->{'biblionumber'}, 1 );
1391 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1392 if ( $item->{'itemlost'} ) {
1393 if (
1394 Koha::RefundLostItemFeeRules->should_refund(
1396 current_branch => C4::Context->userenv->{branch},
1397 item_home_branch => $item->{homebranch},
1398 item_holding_branch => $item->{holdingbranch}
1403 _FixAccountForLostAndReturned( $item->{'itemnumber'}, undef,
1404 $item->{'barcode'} );
1408 ModItem(
1410 issues => $item->{'issues'},
1411 holdingbranch => C4::Context->userenv->{'branch'},
1412 itemlost => 0,
1413 onloan => $datedue->ymd(),
1414 datelastborrowed => DateTime->now( time_zone => C4::Context->tz() )->ymd(),
1416 $item->{'biblionumber'},
1417 $item->{'itemnumber'}
1419 ModDateLastSeen( $item->{'itemnumber'} );
1421 # If it costs to borrow this book, charge it to the patron's account.
1422 my ( $charge, $itemtype ) = GetIssuingCharges( $item->{'itemnumber'}, $borrower->{'borrowernumber'} );
1423 if ( $charge > 0 ) {
1424 AddIssuingCharge( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $charge );
1425 $item->{'charge'} = $charge;
1428 # Record the fact that this book was issued.
1429 &UpdateStats(
1431 branch => C4::Context->userenv->{'branch'},
1432 type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
1433 amount => $charge,
1434 other => ( $sipmode ? "SIP-$sipmode" : '' ),
1435 itemnumber => $item->{'itemnumber'},
1436 itemtype => $item->{'itype'},
1437 borrowernumber => $borrower->{'borrowernumber'},
1438 ccode => $item->{'ccode'}
1442 # Send a checkout slip.
1443 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1444 my %conditions = (
1445 branchcode => $branch,
1446 categorycode => $borrower->{categorycode},
1447 item_type => $item->{itype},
1448 notification => 'CHECKOUT',
1450 if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
1451 SendCirculationAlert(
1453 type => 'CHECKOUT',
1454 item => $item,
1455 borrower => $borrower,
1456 branch => $branch,
1462 logaction(
1463 "CIRCULATION", "ISSUE",
1464 $borrower->{'borrowernumber'},
1465 $biblio->{'itemnumber'}
1466 ) if C4::Context->preference("IssueLog");
1468 return $issue;
1471 =head2 GetLoanLength
1473 my $loanlength = &GetLoanLength($borrowertype,$itemtype,branchcode)
1475 Get loan length for an itemtype, a borrower type and a branch
1477 =cut
1479 sub GetLoanLength {
1480 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1481 my $dbh = C4::Context->dbh;
1482 my $sth = $dbh->prepare(qq{
1483 SELECT issuelength, lengthunit, renewalperiod
1484 FROM issuingrules
1485 WHERE categorycode=?
1486 AND itemtype=?
1487 AND branchcode=?
1488 AND issuelength IS NOT NULL
1491 # try to find issuelength & return the 1st available.
1492 # check with borrowertype, itemtype and branchcode, then without one of those parameters
1493 $sth->execute( $borrowertype, $itemtype, $branchcode );
1494 my $loanlength = $sth->fetchrow_hashref;
1496 return $loanlength
1497 if defined($loanlength) && defined $loanlength->{issuelength};
1499 $sth->execute( $borrowertype, '*', $branchcode );
1500 $loanlength = $sth->fetchrow_hashref;
1501 return $loanlength
1502 if defined($loanlength) && defined $loanlength->{issuelength};
1504 $sth->execute( '*', $itemtype, $branchcode );
1505 $loanlength = $sth->fetchrow_hashref;
1506 return $loanlength
1507 if defined($loanlength) && defined $loanlength->{issuelength};
1509 $sth->execute( '*', '*', $branchcode );
1510 $loanlength = $sth->fetchrow_hashref;
1511 return $loanlength
1512 if defined($loanlength) && defined $loanlength->{issuelength};
1514 $sth->execute( $borrowertype, $itemtype, '*' );
1515 $loanlength = $sth->fetchrow_hashref;
1516 return $loanlength
1517 if defined($loanlength) && defined $loanlength->{issuelength};
1519 $sth->execute( $borrowertype, '*', '*' );
1520 $loanlength = $sth->fetchrow_hashref;
1521 return $loanlength
1522 if defined($loanlength) && defined $loanlength->{issuelength};
1524 $sth->execute( '*', $itemtype, '*' );
1525 $loanlength = $sth->fetchrow_hashref;
1526 return $loanlength
1527 if defined($loanlength) && defined $loanlength->{issuelength};
1529 $sth->execute( '*', '*', '*' );
1530 $loanlength = $sth->fetchrow_hashref;
1531 return $loanlength
1532 if defined($loanlength) && defined $loanlength->{issuelength};
1534 # if no rule is set => 0 day (hardcoded)
1535 return {
1536 issuelength => 0,
1537 renewalperiod => 0,
1538 lengthunit => 'days',
1544 =head2 GetHardDueDate
1546 my ($hardduedate,$hardduedatecompare) = &GetHardDueDate($borrowertype,$itemtype,branchcode)
1548 Get the Hard Due Date and it's comparison for an itemtype, a borrower type and a branch
1550 =cut
1552 sub GetHardDueDate {
1553 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1555 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
1556 { categorycode => $borrowertype,
1557 itemtype => $itemtype,
1558 branchcode => $branchcode
1563 if ( defined( $issuing_rule ) ) {
1564 if ( $issuing_rule->hardduedate ) {
1565 return (dt_from_string($issuing_rule->hardduedate, 'iso'),$issuing_rule->hardduedatecompare);
1566 } else {
1567 return (undef, undef);
1572 =head2 GetBranchBorrowerCircRule
1574 my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
1576 Retrieves circulation rule attributes that apply to the given
1577 branch and patron category, regardless of item type.
1578 The return value is a hashref containing the following key:
1580 maxissueqty - maximum number of loans that a
1581 patron of the given category can have at the given
1582 branch. If the value is undef, no limit.
1584 maxonsiteissueqty - maximum of on-site checkouts that a
1585 patron of the given category can have at the given
1586 branch. If the value is undef, no limit.
1588 This will first check for a specific branch and
1589 category match from branch_borrower_circ_rules.
1591 If no rule is found, it will then check default_branch_circ_rules
1592 (same branch, default category). If no rule is found,
1593 it will then check default_borrower_circ_rules (default
1594 branch, same category), then failing that, default_circ_rules
1595 (default branch, default category).
1597 If no rule has been found in the database, it will default to
1598 the buillt in rule:
1600 maxissueqty - undef
1601 maxonsiteissueqty - undef
1603 C<$branchcode> and C<$categorycode> should contain the
1604 literal branch code and patron category code, respectively - no
1605 wildcards.
1607 =cut
1609 sub GetBranchBorrowerCircRule {
1610 my ( $branchcode, $categorycode ) = @_;
1612 my $rules;
1613 my $dbh = C4::Context->dbh();
1614 $rules = $dbh->selectrow_hashref( q|
1615 SELECT maxissueqty, maxonsiteissueqty
1616 FROM branch_borrower_circ_rules
1617 WHERE branchcode = ?
1618 AND categorycode = ?
1619 |, {}, $branchcode, $categorycode ) ;
1620 return $rules if $rules;
1622 # try same branch, default borrower category
1623 $rules = $dbh->selectrow_hashref( q|
1624 SELECT maxissueqty, maxonsiteissueqty
1625 FROM default_branch_circ_rules
1626 WHERE branchcode = ?
1627 |, {}, $branchcode ) ;
1628 return $rules if $rules;
1630 # try default branch, same borrower category
1631 $rules = $dbh->selectrow_hashref( q|
1632 SELECT maxissueqty, maxonsiteissueqty
1633 FROM default_borrower_circ_rules
1634 WHERE categorycode = ?
1635 |, {}, $categorycode ) ;
1636 return $rules if $rules;
1638 # try default branch, default borrower category
1639 $rules = $dbh->selectrow_hashref( q|
1640 SELECT maxissueqty, maxonsiteissueqty
1641 FROM default_circ_rules
1642 |, {} );
1643 return $rules if $rules;
1645 # built-in default circulation rule
1646 return {
1647 maxissueqty => undef,
1648 maxonsiteissueqty => undef,
1652 =head2 GetBranchItemRule
1654 my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
1656 Retrieves circulation rule attributes that apply to the given
1657 branch and item type, regardless of patron category.
1659 The return value is a hashref containing the following keys:
1661 holdallowed => Hold policy for this branch and itemtype. Possible values:
1662 0: No holds allowed.
1663 1: Holds allowed only by patrons that have the same homebranch as the item.
1664 2: Holds allowed from any patron.
1666 returnbranch => branch to which to return item. Possible values:
1667 noreturn: do not return, let item remain where checked in (floating collections)
1668 homebranch: return to item's home branch
1669 holdingbranch: return to issuer branch
1671 This searches branchitemrules in the following order:
1673 * Same branchcode and itemtype
1674 * Same branchcode, itemtype '*'
1675 * branchcode '*', same itemtype
1676 * branchcode and itemtype '*'
1678 Neither C<$branchcode> nor C<$itemtype> should be '*'.
1680 =cut
1682 sub GetBranchItemRule {
1683 my ( $branchcode, $itemtype ) = @_;
1684 my $dbh = C4::Context->dbh();
1685 my $result = {};
1687 my @attempts = (
1688 ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1689 FROM branch_item_rules
1690 WHERE branchcode = ?
1691 AND itemtype = ?', $branchcode, $itemtype],
1692 ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1693 FROM default_branch_circ_rules
1694 WHERE branchcode = ?', $branchcode],
1695 ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1696 FROM default_branch_item_rules
1697 WHERE itemtype = ?', $itemtype],
1698 ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1699 FROM default_circ_rules'],
1702 foreach my $attempt (@attempts) {
1703 my ($query, @bind_params) = @{$attempt};
1704 my $search_result = $dbh->selectrow_hashref ( $query , {}, @bind_params )
1705 or next;
1707 # Since branch/category and branch/itemtype use the same per-branch
1708 # defaults tables, we have to check that the key we want is set, not
1709 # just that a row was returned
1710 $result->{'holdallowed'} = $search_result->{'holdallowed'} unless ( defined $result->{'holdallowed'} );
1711 $result->{'hold_fulfillment_policy'} = $search_result->{'hold_fulfillment_policy'} unless ( defined $result->{'hold_fulfillment_policy'} );
1712 $result->{'returnbranch'} = $search_result->{'returnbranch'} unless ( defined $result->{'returnbranch'} );
1715 # built-in default circulation rule
1716 $result->{'holdallowed'} = 2 unless ( defined $result->{'holdallowed'} );
1717 $result->{'hold_fulfillment_policy'} = 'any' unless ( defined $result->{'hold_fulfillment_policy'} );
1718 $result->{'returnbranch'} = 'homebranch' unless ( defined $result->{'returnbranch'} );
1720 return $result;
1723 =head2 AddReturn
1725 ($doreturn, $messages, $iteminformation, $borrower) =
1726 &AddReturn( $barcode, $branch [,$exemptfine] [,$dropbox] [,$returndate] );
1728 Returns a book.
1730 =over 4
1732 =item C<$barcode> is the bar code of the book being returned.
1734 =item C<$branch> is the code of the branch where the book is being returned.
1736 =item C<$exemptfine> indicates that overdue charges for the item will be
1737 removed. Optional.
1739 =item C<$dropbox> indicates that the check-in date is assumed to be
1740 yesterday, or the last non-holiday as defined in C4::Calendar . If
1741 overdue charges are applied and C<$dropbox> is true, the last charge
1742 will be removed. This assumes that the fines accrual script has run
1743 for _today_. Optional.
1745 =item C<$return_date> allows the default return date to be overridden
1746 by the given return date. Optional.
1748 =back
1750 C<&AddReturn> returns a list of four items:
1752 C<$doreturn> is true iff the return succeeded.
1754 C<$messages> is a reference-to-hash giving feedback on the operation.
1755 The keys of the hash are:
1757 =over 4
1759 =item C<BadBarcode>
1761 No item with this barcode exists. The value is C<$barcode>.
1763 =item C<NotIssued>
1765 The book is not currently on loan. The value is C<$barcode>.
1767 =item C<IsPermanent>
1769 The book's home branch is a permanent collection. If you have borrowed
1770 this book, you are not allowed to return it. The value is the code for
1771 the book's home branch.
1773 =item C<withdrawn>
1775 This book has been withdrawn/cancelled. The value should be ignored.
1777 =item C<Wrongbranch>
1779 This book has was returned to the wrong branch. The value is a hashref
1780 so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
1781 contain the branchcode of the incorrect and correct return library, respectively.
1783 =item C<ResFound>
1785 The item was reserved. The value is a reference-to-hash whose keys are
1786 fields from the reserves table of the Koha database, and
1787 C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
1788 either C<Waiting>, C<Reserved>, or 0.
1790 =item C<WasReturned>
1792 Value 1 if return is successful.
1794 =item C<NeedsTransfer>
1796 If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer.
1798 =back
1800 C<$iteminformation> is a reference-to-hash, giving information about the
1801 returned item from the issues table.
1803 C<$borrower> is a reference-to-hash, giving information about the
1804 patron who last borrowed the book.
1806 =cut
1808 sub AddReturn {
1809 my ( $barcode, $branch, $exemptfine, $dropbox, $return_date, $dropboxdate ) = @_;
1811 if ($branch and not Koha::Libraries->find($branch)) {
1812 warn "AddReturn error: branch '$branch' not found. Reverting to " . C4::Context->userenv->{'branch'};
1813 undef $branch;
1815 $branch = C4::Context->userenv->{'branch'} unless $branch; # we trust userenv to be a safe fallback/default
1816 my $messages;
1817 my $borrower;
1818 my $doreturn = 1;
1819 my $validTransfert = 0;
1820 my $stat_type = 'return';
1822 # get information on item
1823 my $item = GetItem( undef, $barcode );
1824 unless ($item) {
1825 return ( 0, { BadBarcode => $barcode } ); # no barcode means no item or borrower. bail out.
1828 my $itemnumber = $item->{ itemnumber };
1830 my $item_level_itypes = C4::Context->preference("item-level_itypes");
1831 my $biblio = $item_level_itypes ? undef : GetBiblioData( $item->{ biblionumber } ); # don't get bib data unless we need it
1832 my $itemtype = $item_level_itypes ? $item->{itype} : $biblio->{itemtype};
1834 my $issue = GetItemIssue($itemnumber);
1835 if ($issue and $issue->{borrowernumber}) {
1836 $borrower = C4::Members::GetMember( borrowernumber => $issue->{borrowernumber} )
1837 or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existent borrowernumber '$issue->{borrowernumber}'\n"
1838 . Dumper($issue) . "\n";
1839 } else {
1840 $messages->{'NotIssued'} = $barcode;
1841 # even though item is not on loan, it may still be transferred; therefore, get current branch info
1842 $doreturn = 0;
1843 # No issue, no borrowernumber. ONLY if $doreturn, *might* you have a $borrower later.
1844 # Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
1845 if (C4::Context->preference("RecordLocalUseOnReturn")) {
1846 $messages->{'LocalUse'} = 1;
1847 $stat_type = 'localuse';
1851 if ( $item->{'location'} eq 'PROC' ) {
1852 if ( C4::Context->preference("InProcessingToShelvingCart") ) {
1853 $item->{'location'} = 'CART';
1855 else {
1856 $item->{location} = $item->{permanent_location};
1859 ModItem( $item, $item->{'biblionumber'}, $item->{'itemnumber'} );
1862 # full item data, but no borrowernumber or checkout info (no issue)
1863 # we know GetItem should work because GetItemnumberFromBarcode worked
1864 my $hbr = GetBranchItemRule($item->{'homebranch'}, $item->{'itype'})->{'returnbranch'} || "homebranch";
1865 # get the proper branch to which to return the item
1866 my $returnbranch = $item->{$hbr} || $branch ;
1867 # if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
1869 my $borrowernumber = $borrower->{'borrowernumber'} || undef; # we don't know if we had a borrower or not
1871 my $yaml = C4::Context->preference('UpdateNotForLoanStatusOnCheckin');
1872 if ($yaml) {
1873 $yaml = "$yaml\n\n"; # YAML is anal on ending \n. Surplus does not hurt
1874 my $rules;
1875 eval { $rules = YAML::Load($yaml); };
1876 if ($@) {
1877 warn "Unable to parse UpdateNotForLoanStatusOnCheckin syspref : $@";
1879 else {
1880 foreach my $key ( keys %$rules ) {
1881 if ( $item->{notforloan} eq $key ) {
1882 $messages->{'NotForLoanStatusUpdated'} = { from => $item->{notforloan}, to => $rules->{$key} };
1883 ModItem( { notforloan => $rules->{$key} }, undef, $itemnumber );
1884 last;
1891 # check if the book is in a permanent collection....
1892 # FIXME -- This 'PE' attribute is largely undocumented. afaict, there's no user interface that reflects this functionality.
1893 if ( $returnbranch ) {
1894 my $library = Koha::Libraries->find($returnbranch);
1895 if ( $library and $library->get_categories->search({'me.categorycode' => 'PE'})->count ) {
1896 $messages->{'IsPermanent'} = $returnbranch;
1900 # check if the return is allowed at this branch
1901 my ($returnallowed, $message) = CanBookBeReturned($item, $branch);
1902 unless ($returnallowed){
1903 $messages->{'Wrongbranch'} = {
1904 Wrongbranch => $branch,
1905 Rightbranch => $message
1907 $doreturn = 0;
1908 return ( $doreturn, $messages, $issue, $borrower );
1911 if ( $item->{'withdrawn'} ) { # book has been cancelled
1912 $messages->{'withdrawn'} = 1;
1913 $doreturn = 0 if C4::Context->preference("BlockReturnOfWithdrawnItems");
1916 # case of a return of document (deal with issues and holdingbranch)
1917 my $today = DateTime->now( time_zone => C4::Context->tz() );
1919 if ($doreturn) {
1920 my $datedue = $issue->{date_due};
1921 $borrower or warn "AddReturn without current borrower";
1922 my $circControlBranch;
1923 if ($dropbox) {
1924 # define circControlBranch only if dropbox mode is set
1925 # don't allow dropbox mode to create an invalid entry in issues (issuedate > today)
1926 # FIXME: check issuedate > returndate, factoring in holidays
1928 $circControlBranch = _GetCircControlBranch($item,$borrower);
1929 $issue->{'overdue'} = DateTime->compare($issue->{'date_due'}, $dropboxdate ) == -1 ? 1 : 0;
1932 if ($borrowernumber) {
1933 if ( ( C4::Context->preference('CalculateFinesOnReturn') && $issue->{'overdue'} ) || $return_date ) {
1934 _CalculateAndUpdateFine( { issue => $issue, item => $item, borrower => $borrower, return_date => $return_date } );
1937 eval {
1938 MarkIssueReturned( $borrowernumber, $item->{'itemnumber'},
1939 $circControlBranch, $return_date, $borrower->{'privacy'} );
1941 if ( $@ ) {
1942 $messages->{'Wrongbranch'} = {
1943 Wrongbranch => $branch,
1944 Rightbranch => $message
1946 carp $@;
1947 return ( 0, { WasReturned => 0 }, $issue, $borrower );
1950 # FIXME is the "= 1" right? This could be the borrower hash.
1951 $messages->{'WasReturned'} = 1;
1955 ModItem({ onloan => undef }, $issue->{'biblionumber'}, $item->{'itemnumber'});
1958 # the holdingbranch is updated if the document is returned to another location.
1959 # this is always done regardless of whether the item was on loan or not
1960 my $item_holding_branch = $item->{ holdingbranch };
1961 if ($item->{'holdingbranch'} ne $branch) {
1962 UpdateHoldingbranch($branch, $item->{'itemnumber'});
1963 $item->{'holdingbranch'} = $branch; # update item data holdingbranch too
1965 ModDateLastSeen( $item->{'itemnumber'} );
1967 # check if we have a transfer for this document
1968 my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->{'itemnumber'} );
1970 # if we have a transfer to do, we update the line of transfers with the datearrived
1971 my $is_in_rotating_collection = C4::RotatingCollections::isItemInAnyCollection( $item->{'itemnumber'} );
1972 if ($datesent) {
1973 if ( $tobranch eq $branch ) {
1974 my $sth = C4::Context->dbh->prepare(
1975 "UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL"
1977 $sth->execute( $item->{'itemnumber'} );
1978 # if we have a reservation with valid transfer, we can set it's status to 'W'
1979 ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
1980 C4::Reserves::ModReserveStatus($item->{'itemnumber'}, 'W');
1981 } else {
1982 $messages->{'WrongTransfer'} = $tobranch;
1983 $messages->{'WrongTransferItem'} = $item->{'itemnumber'};
1985 $validTransfert = 1;
1986 } else {
1987 ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
1990 # fix up the accounts.....
1991 if ( $item->{'itemlost'} ) {
1992 $messages->{'WasLost'} = 1;
1994 if ( $item->{'itemlost'} ) {
1995 if (
1996 Koha::RefundLostItemFeeRules->should_refund(
1998 current_branch => C4::Context->userenv->{branch},
1999 item_home_branch => $item->{homebranch},
2000 item_holding_branch => $item_holding_branch
2005 _FixAccountForLostAndReturned( $item->{'itemnumber'}, $borrowernumber, $barcode );
2006 $messages->{'LostItemFeeRefunded'} = 1;
2011 # fix up the overdues in accounts...
2012 if ($borrowernumber) {
2013 my $fix = _FixOverduesOnReturn($borrowernumber, $item->{itemnumber}, $exemptfine, $dropbox);
2014 defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $item->{itemnumber}...) failed!"; # zero is OK, check defined
2016 if ( $issue->{overdue} && $issue->{date_due} ) {
2017 # fix fine days
2018 $today = $dropboxdate if $dropbox;
2019 my ($debardate,$reminder) = _debar_user_on_return( $borrower, $item, $issue->{date_due}, $today );
2020 if ($reminder){
2021 $messages->{'PrevDebarred'} = $debardate;
2022 } else {
2023 $messages->{'Debarred'} = $debardate if $debardate;
2025 # there's no overdue on the item but borrower had been previously debarred
2026 } elsif ( $issue->{date_due} and $borrower->{'debarred'} ) {
2027 if ( $borrower->{debarred} eq "9999-12-31") {
2028 $messages->{'ForeverDebarred'} = $borrower->{'debarred'};
2029 } else {
2030 my $borrower_debar_dt = dt_from_string( $borrower->{debarred} );
2031 $borrower_debar_dt->truncate(to => 'day');
2032 my $today_dt = $today->clone()->truncate(to => 'day');
2033 if ( DateTime->compare( $borrower_debar_dt, $today_dt ) != -1 ) {
2034 $messages->{'PrevDebarred'} = $borrower->{'debarred'};
2040 # find reserves.....
2041 # if we don't have a reserve with the status W, we launch the Checkreserves routine
2042 my ($resfound, $resrec);
2043 my $lookahead= C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
2044 ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->{'itemnumber'}, undef, $lookahead ) unless ( $item->{'withdrawn'} );
2045 if ($resfound) {
2046 $resrec->{'ResFound'} = $resfound;
2047 $messages->{'ResFound'} = $resrec;
2050 # Record the fact that this book was returned.
2051 UpdateStats({
2052 branch => $branch,
2053 type => $stat_type,
2054 itemnumber => $itemnumber,
2055 itemtype => $itemtype,
2056 borrowernumber => $borrowernumber,
2057 ccode => $item->{ ccode }
2060 # Send a check-in slip. # NOTE: borrower may be undef. probably shouldn't try to send messages then.
2061 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2062 my %conditions = (
2063 branchcode => $branch,
2064 categorycode => $borrower->{categorycode},
2065 item_type => $item->{itype},
2066 notification => 'CHECKIN',
2068 if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
2069 SendCirculationAlert({
2070 type => 'CHECKIN',
2071 item => $item,
2072 borrower => $borrower,
2073 branch => $branch,
2077 logaction("CIRCULATION", "RETURN", $borrowernumber, $item->{'itemnumber'})
2078 if C4::Context->preference("ReturnLog");
2080 # Remove any OVERDUES related debarment if the borrower has no overdues
2081 if ( $borrowernumber
2082 && $borrower->{'debarred'}
2083 && C4::Context->preference('AutoRemoveOverduesRestrictions')
2084 && !Koha::Patrons->find( $borrowernumber )->has_overdues
2085 && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2087 DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2090 # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
2091 if (!$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) and not $messages->{'WrongTransfer'}){
2092 if (C4::Context->preference("AutomaticItemReturn" ) or
2093 (C4::Context->preference("UseBranchTransferLimits") and
2094 ! IsBranchTransferAllowed($branch, $returnbranch, $item->{C4::Context->preference("BranchTransferLimitsType")} )
2095 )) {
2096 $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s)", $item->{'itemnumber'},$branch, $returnbranch;
2097 $debug and warn "item: " . Dumper($item);
2098 ModItemTransfer($item->{'itemnumber'}, $branch, $returnbranch);
2099 $messages->{'WasTransfered'} = 1;
2100 } else {
2101 $messages->{'NeedsTransfer'} = $returnbranch;
2105 return ( $doreturn, $messages, $issue, $borrower );
2108 =head2 MarkIssueReturned
2110 MarkIssueReturned($borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy);
2112 Unconditionally marks an issue as being returned by
2113 moving the C<issues> row to C<old_issues> and
2114 setting C<returndate> to the current date, or
2115 the last non-holiday date of the branccode specified in
2116 C<dropbox_branch> . Assumes you've already checked that
2117 it's safe to do this, i.e. last non-holiday > issuedate.
2119 if C<$returndate> is specified (in iso format), it is used as the date
2120 of the return. It is ignored when a dropbox_branch is passed in.
2122 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
2123 the old_issue is immediately anonymised
2125 Ideally, this function would be internal to C<C4::Circulation>,
2126 not exported, but it is currently needed by one
2127 routine in C<C4::Accounts>.
2129 =cut
2131 sub MarkIssueReturned {
2132 my ( $borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy ) = @_;
2134 my $anonymouspatron;
2135 if ( $privacy == 2 ) {
2136 # The default of 0 will not work due to foreign key constraints
2137 # The anonymisation will fail if AnonymousPatron is not a valid entry
2138 # We need to check if the anonymous patron exist, Koha will fail loudly if it does not
2139 # Note that a warning should appear on the about page (System information tab).
2140 $anonymouspatron = C4::Context->preference('AnonymousPatron');
2141 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."
2142 unless C4::Members::GetMember( borrowernumber => $anonymouspatron );
2144 my $dbh = C4::Context->dbh;
2145 my $query = 'UPDATE issues SET returndate=';
2146 my @bind;
2147 if ($dropbox_branch) {
2148 my $calendar = Koha::Calendar->new( branchcode => $dropbox_branch );
2149 my $dropboxdate = $calendar->addDate( DateTime->now( time_zone => C4::Context->tz), -1 );
2150 $query .= ' ? ';
2151 push @bind, $dropboxdate->strftime('%Y-%m-%d %H:%M');
2152 } elsif ($returndate) {
2153 $query .= ' ? ';
2154 push @bind, $returndate;
2155 } else {
2156 $query .= ' now() ';
2158 $query .= ' WHERE borrowernumber = ? AND itemnumber = ?';
2159 push @bind, $borrowernumber, $itemnumber;
2160 # FIXME transaction
2161 my $sth_upd = $dbh->prepare($query);
2162 $sth_upd->execute(@bind);
2163 my $sth_copy = $dbh->prepare('INSERT INTO old_issues SELECT * FROM issues
2164 WHERE borrowernumber = ?
2165 AND itemnumber = ?');
2166 $sth_copy->execute($borrowernumber, $itemnumber);
2167 # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
2168 if ( $privacy == 2) {
2169 my $sth_ano = $dbh->prepare("UPDATE old_issues SET borrowernumber=?
2170 WHERE borrowernumber = ?
2171 AND itemnumber = ?");
2172 $sth_ano->execute($anonymouspatron, $borrowernumber, $itemnumber);
2174 my $sth_del = $dbh->prepare("DELETE FROM issues
2175 WHERE borrowernumber = ?
2176 AND itemnumber = ?");
2177 $sth_del->execute($borrowernumber, $itemnumber);
2179 ModItem( { 'onloan' => undef }, undef, $itemnumber );
2181 if ( C4::Context->preference('StoreLastBorrower') ) {
2182 my $item = Koha::Items->find( $itemnumber );
2183 my $patron = Koha::Patrons->find( $borrowernumber );
2184 $item->last_returned_by( $patron );
2188 =head2 _debar_user_on_return
2190 _debar_user_on_return($borrower, $item, $datedue, today);
2192 C<$borrower> borrower hashref
2194 C<$item> item hashref
2196 C<$datedue> date due DateTime object
2198 C<$today> DateTime object representing the return time
2200 Internal function, called only by AddReturn that calculates and updates
2201 the user fine days, and debars him if necessary.
2203 Should only be called for overdue returns
2205 =cut
2207 sub _debar_user_on_return {
2208 my ( $borrower, $item, $dt_due, $dt_today ) = @_;
2210 my $branchcode = _GetCircControlBranch( $item, $borrower );
2212 my $circcontrol = C4::Context->preference('CircControl');
2213 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
2214 { categorycode => $borrower->{categorycode},
2215 itemtype => $item->{itype},
2216 branchcode => $branchcode
2219 my $finedays = $issuing_rule ? $issuing_rule->finedays : undef;
2220 my $unit = $issuing_rule ? $issuing_rule->lengthunit : undef;
2221 my $chargeable_units = C4::Overdues::get_chargeable_units($unit, $dt_due, $dt_today, $branchcode);
2223 if ($finedays) {
2225 # finedays is in days, so hourly loans must multiply by 24
2226 # thus 1 hour late equals 1 day suspension * finedays rate
2227 $finedays = $finedays * 24 if ( $unit eq 'hours' );
2229 # grace period is measured in the same units as the loan
2230 my $grace =
2231 DateTime::Duration->new( $unit => $issuing_rule->firstremind );
2233 my $deltadays = DateTime::Duration->new(
2234 days => $chargeable_units
2236 if ( $deltadays->subtract($grace)->is_positive() ) {
2237 my $suspension_days = $deltadays * $finedays;
2239 # If the max suspension days is < than the suspension days
2240 # the suspension days is limited to this maximum period.
2241 my $max_sd = $issuing_rule->maxsuspensiondays;
2242 if ( defined $max_sd ) {
2243 $max_sd = DateTime::Duration->new( days => $max_sd );
2244 $suspension_days = $max_sd
2245 if DateTime::Duration->compare( $max_sd, $suspension_days ) < 0;
2248 my $new_debar_dt =
2249 $dt_today->clone()->add_duration( $suspension_days );
2251 Koha::Patron::Debarments::AddUniqueDebarment({
2252 borrowernumber => $borrower->{borrowernumber},
2253 expiration => $new_debar_dt->ymd(),
2254 type => 'SUSPENSION',
2256 # if borrower was already debarred but does not get an extra debarment
2257 my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
2258 if ( $borrower->{debarred} eq $patron->is_debarred ) {
2259 return ($borrower->{debarred},1);
2261 return $new_debar_dt->ymd();
2264 return;
2267 =head2 _FixOverduesOnReturn
2269 &_FixOverduesOnReturn($brn,$itm, $exemptfine, $dropboxmode);
2271 C<$brn> borrowernumber
2273 C<$itm> itemnumber
2275 C<$exemptfine> BOOL -- remove overdue charge associated with this issue.
2276 C<$dropboxmode> BOOL -- remove lastincrement on overdue charge associated with this issue.
2278 Internal function, called only by AddReturn
2280 =cut
2282 sub _FixOverduesOnReturn {
2283 my ($borrowernumber, $item);
2284 unless ($borrowernumber = shift) {
2285 warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
2286 return;
2288 unless ($item = shift) {
2289 warn "_FixOverduesOnReturn() not supplied valid itemnumber";
2290 return;
2292 my ($exemptfine, $dropbox) = @_;
2293 my $dbh = C4::Context->dbh;
2295 # check for overdue fine
2296 my $sth = $dbh->prepare(
2297 "SELECT * FROM accountlines WHERE (borrowernumber = ?) AND (itemnumber = ?) AND (accounttype='FU' OR accounttype='O')"
2299 $sth->execute( $borrowernumber, $item );
2301 # alter fine to show that the book has been returned
2302 my $data = $sth->fetchrow_hashref;
2303 return 0 unless $data; # no warning, there's just nothing to fix
2305 my $uquery;
2306 my @bind = ($data->{'accountlines_id'});
2307 if ($exemptfine) {
2308 $uquery = "update accountlines set accounttype='FFOR', amountoutstanding=0";
2309 if (C4::Context->preference("FinesLog")) {
2310 &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
2312 } elsif ($dropbox && $data->{lastincrement}) {
2313 my $outstanding = $data->{amountoutstanding} - $data->{lastincrement} ;
2314 my $amt = $data->{amount} - $data->{lastincrement} ;
2315 if (C4::Context->preference("FinesLog")) {
2316 &logaction("FINES", 'MODIFY',$borrowernumber,"Dropbox adjustment $amt, item $item");
2318 $uquery = "update accountlines set accounttype='F' ";
2319 if($outstanding >= 0 && $amt >=0) {
2320 $uquery .= ", amount = ? , amountoutstanding=? ";
2321 unshift @bind, ($amt, $outstanding) ;
2323 } else {
2324 $uquery = "update accountlines set accounttype='F' ";
2326 $uquery .= " where (accountlines_id = ?)";
2327 my $usth = $dbh->prepare($uquery);
2328 return $usth->execute(@bind);
2331 =head2 _FixAccountForLostAndReturned
2333 &_FixAccountForLostAndReturned($itemnumber, [$borrowernumber, $barcode]);
2335 Calculates the charge for a book lost and returned.
2337 Internal function, not exported, called only by AddReturn.
2339 FIXME: This function reflects how inscrutable fines logic is. Fix both.
2340 FIXME: Give a positive return value on success. It might be the $borrowernumber who received credit, or the amount forgiven.
2342 =cut
2344 sub _FixAccountForLostAndReturned {
2345 my $itemnumber = shift or return;
2346 my $borrowernumber = @_ ? shift : undef;
2347 my $item_id = @_ ? shift : $itemnumber; # Send the barcode if you want that logged in the description
2348 my $dbh = C4::Context->dbh;
2349 # check for charge made for lost book
2350 my $sth = $dbh->prepare("SELECT * FROM accountlines WHERE itemnumber = ? AND accounttype IN ('L', 'Rep', 'W') ORDER BY date DESC, accountno DESC");
2351 $sth->execute($itemnumber);
2352 my $data = $sth->fetchrow_hashref;
2353 $data or return; # bail if there is nothing to do
2354 $data->{accounttype} eq 'W' and return; # Written off
2356 # writeoff this amount
2357 my $offset;
2358 my $amount = $data->{'amount'};
2359 my $acctno = $data->{'accountno'};
2360 my $amountleft; # Starts off undef/zero.
2361 if ($data->{'amountoutstanding'} == $amount) {
2362 $offset = $data->{'amount'};
2363 $amountleft = 0; # Hey, it's zero here, too.
2364 } else {
2365 $offset = $amount - $data->{'amountoutstanding'}; # Um, isn't this the same as ZERO? We just tested those two things are ==
2366 $amountleft = $data->{'amountoutstanding'} - $amount; # Um, isn't this the same as ZERO? We just tested those two things are ==
2368 my $usth = $dbh->prepare("UPDATE accountlines SET accounttype = 'LR',amountoutstanding='0'
2369 WHERE (accountlines_id = ?)");
2370 $usth->execute($data->{'accountlines_id'}); # We might be adjusting an account for some OTHER borrowernumber now. Not the one we passed in.
2371 #check if any credit is left if so writeoff other accounts
2372 my $nextaccntno = getnextacctno($data->{'borrowernumber'});
2373 $amountleft *= -1 if ($amountleft < 0);
2374 if ($amountleft > 0) {
2375 my $msth = $dbh->prepare("SELECT * FROM accountlines WHERE (borrowernumber = ?)
2376 AND (amountoutstanding >0) ORDER BY date"); # might want to order by amountoustanding ASC (pay smallest first)
2377 $msth->execute($data->{'borrowernumber'});
2378 # offset transactions
2379 my $newamtos;
2380 my $accdata;
2381 while (($accdata=$msth->fetchrow_hashref) and ($amountleft>0)){
2382 if ($accdata->{'amountoutstanding'} < $amountleft) {
2383 $newamtos = 0;
2384 $amountleft -= $accdata->{'amountoutstanding'};
2385 } else {
2386 $newamtos = $accdata->{'amountoutstanding'} - $amountleft;
2387 $amountleft = 0;
2389 my $thisacct = $accdata->{'accountlines_id'};
2390 # FIXME: move prepares outside while loop!
2391 my $usth = $dbh->prepare("UPDATE accountlines SET amountoutstanding= ?
2392 WHERE (accountlines_id = ?)");
2393 $usth->execute($newamtos,$thisacct);
2394 $usth = $dbh->prepare("INSERT INTO accountoffsets
2395 (borrowernumber, accountno, offsetaccount, offsetamount)
2396 VALUES
2397 (?,?,?,?)");
2398 $usth->execute($data->{'borrowernumber'},$accdata->{'accountno'},$nextaccntno,$newamtos);
2401 $amountleft *= -1 if ($amountleft > 0);
2402 my $desc = "Item Returned " . $item_id;
2403 $usth = $dbh->prepare("INSERT INTO accountlines
2404 (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
2405 VALUES (?,?,now(),?,?,'CR',?)");
2406 $usth->execute($data->{'borrowernumber'},$nextaccntno,0-$amount,$desc,$amountleft);
2407 if ($borrowernumber) {
2408 # FIXME: same as query above. use 1 sth for both
2409 $usth = $dbh->prepare("INSERT INTO accountoffsets
2410 (borrowernumber, accountno, offsetaccount, offsetamount)
2411 VALUES (?,?,?,?)");
2412 $usth->execute($borrowernumber, $data->{'accountno'}, $nextaccntno, $offset);
2414 ModItem({ paidfor => '' }, undef, $itemnumber);
2415 return;
2418 =head2 _GetCircControlBranch
2420 my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
2422 Internal function :
2424 Return the library code to be used to determine which circulation
2425 policy applies to a transaction. Looks up the CircControl and
2426 HomeOrHoldingBranch system preferences.
2428 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
2430 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
2432 =cut
2434 sub _GetCircControlBranch {
2435 my ($item, $borrower) = @_;
2436 my $circcontrol = C4::Context->preference('CircControl');
2437 my $branch;
2439 if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
2440 $branch= C4::Context->userenv->{'branch'};
2441 } elsif ($circcontrol eq 'PatronLibrary') {
2442 $branch=$borrower->{branchcode};
2443 } else {
2444 my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
2445 $branch = $item->{$branchfield};
2446 # default to item home branch if holdingbranch is used
2447 # and is not defined
2448 if (!defined($branch) && $branchfield eq 'holdingbranch') {
2449 $branch = $item->{homebranch};
2452 return $branch;
2460 =head2 GetItemIssue
2462 $issue = &GetItemIssue($itemnumber);
2464 Returns patron currently having a book, or undef if not checked out.
2466 C<$itemnumber> is the itemnumber.
2468 C<$issue> is a hashref of the row from the issues table.
2470 =cut
2472 sub GetItemIssue {
2473 my ($itemnumber) = @_;
2474 return unless $itemnumber;
2475 my $sth = C4::Context->dbh->prepare(
2476 "SELECT items.*, issues.*
2477 FROM issues
2478 LEFT JOIN items ON issues.itemnumber=items.itemnumber
2479 WHERE issues.itemnumber=?");
2480 $sth->execute($itemnumber);
2481 my $data = $sth->fetchrow_hashref;
2482 return unless $data;
2483 $data->{issuedate_sql} = $data->{issuedate};
2484 $data->{date_due_sql} = $data->{date_due};
2485 $data->{issuedate} = dt_from_string($data->{issuedate}, 'sql');
2486 $data->{issuedate}->truncate(to => 'minute');
2487 $data->{date_due} = dt_from_string($data->{date_due}, 'sql');
2488 $data->{date_due}->truncate(to => 'minute');
2489 my $dt = DateTime->now( time_zone => C4::Context->tz)->truncate( to => 'minute');
2490 $data->{'overdue'} = DateTime->compare($data->{'date_due'}, $dt ) == -1 ? 1 : 0;
2491 return $data;
2494 =head2 GetOpenIssue
2496 $issue = GetOpenIssue( $itemnumber );
2498 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2500 C<$itemnumber> is the item's itemnumber
2502 Returns a hashref
2504 =cut
2506 sub GetOpenIssue {
2507 my ( $itemnumber ) = @_;
2508 return unless $itemnumber;
2509 my $dbh = C4::Context->dbh;
2510 my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2511 $sth->execute( $itemnumber );
2512 return $sth->fetchrow_hashref();
2516 =head2 GetItemIssues
2518 $issues = &GetItemIssues($itemnumber, $history);
2520 Returns patrons that have issued a book
2522 C<$itemnumber> is the itemnumber
2523 C<$history> is false if you just want the current "issuer" (if any)
2524 and true if you want issues history from old_issues also.
2526 Returns reference to an array of hashes
2528 =cut
2530 sub GetItemIssues {
2531 my ( $itemnumber, $history ) = @_;
2533 my $today = DateTime->now( time_zome => C4::Context->tz); # get today date
2534 $today->truncate( to => 'minute' );
2535 my $sql = "SELECT * FROM issues
2536 JOIN borrowers USING (borrowernumber)
2537 JOIN items USING (itemnumber)
2538 WHERE issues.itemnumber = ? ";
2539 if ($history) {
2540 $sql .= "UNION ALL
2541 SELECT * FROM old_issues
2542 LEFT JOIN borrowers USING (borrowernumber)
2543 JOIN items USING (itemnumber)
2544 WHERE old_issues.itemnumber = ? ";
2546 $sql .= "ORDER BY date_due DESC";
2547 my $sth = C4::Context->dbh->prepare($sql);
2548 if ($history) {
2549 $sth->execute($itemnumber, $itemnumber);
2550 } else {
2551 $sth->execute($itemnumber);
2553 my $results = $sth->fetchall_arrayref({});
2554 foreach (@$results) {
2555 my $date_due = dt_from_string($_->{date_due},'sql');
2556 $date_due->truncate( to => 'minute' );
2558 $_->{overdue} = (DateTime->compare($date_due, $today) == -1) ? 1 : 0;
2560 return $results;
2563 =head2 GetBiblioIssues
2565 $issues = GetBiblioIssues($biblionumber);
2567 this function get all issues from a biblionumber.
2569 Return:
2570 C<$issues> is a reference to array which each value is ref-to-hash. This ref-to-hash containts all column from
2571 tables issues and the firstname,surname & cardnumber from borrowers.
2573 =cut
2575 sub GetBiblioIssues {
2576 my $biblionumber = shift;
2577 return unless $biblionumber;
2578 my $dbh = C4::Context->dbh;
2579 my $query = "
2580 SELECT issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2581 FROM issues
2582 LEFT JOIN borrowers ON borrowers.borrowernumber = issues.borrowernumber
2583 LEFT JOIN items ON issues.itemnumber = items.itemnumber
2584 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2585 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2586 WHERE biblio.biblionumber = ?
2587 UNION ALL
2588 SELECT old_issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2589 FROM old_issues
2590 LEFT JOIN borrowers ON borrowers.borrowernumber = old_issues.borrowernumber
2591 LEFT JOIN items ON old_issues.itemnumber = items.itemnumber
2592 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2593 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2594 WHERE biblio.biblionumber = ?
2595 ORDER BY timestamp
2597 my $sth = $dbh->prepare($query);
2598 $sth->execute($biblionumber, $biblionumber);
2600 my @issues;
2601 while ( my $data = $sth->fetchrow_hashref ) {
2602 push @issues, $data;
2604 return \@issues;
2607 =head2 GetUpcomingDueIssues
2609 my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2611 =cut
2613 sub GetUpcomingDueIssues {
2614 my $params = shift;
2616 $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2617 my $dbh = C4::Context->dbh;
2619 my $statement = <<END_SQL;
2620 SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2621 FROM issues
2622 LEFT JOIN items USING (itemnumber)
2623 LEFT OUTER JOIN branches USING (branchcode)
2624 WHERE returndate is NULL
2625 HAVING days_until_due >= 0 AND days_until_due <= ?
2626 END_SQL
2628 my @bind_parameters = ( $params->{'days_in_advance'} );
2630 my $sth = $dbh->prepare( $statement );
2631 $sth->execute( @bind_parameters );
2632 my $upcoming_dues = $sth->fetchall_arrayref({});
2634 return $upcoming_dues;
2637 =head2 CanBookBeRenewed
2639 ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber[, $override_limit]);
2641 Find out whether a borrowed item may be renewed.
2643 C<$borrowernumber> is the borrower number of the patron who currently
2644 has the item on loan.
2646 C<$itemnumber> is the number of the item to renew.
2648 C<$override_limit>, if supplied with a true value, causes
2649 the limit on the number of times that the loan can be renewed
2650 (as controlled by the item type) to be ignored. Overriding also allows
2651 to renew sooner than "No renewal before" and to manually renew loans
2652 that are automatically renewed.
2654 C<$CanBookBeRenewed> returns a true value if the item may be renewed. The
2655 item must currently be on loan to the specified borrower; renewals
2656 must be allowed for the item's type; and the borrower must not have
2657 already renewed the loan. $error will contain the reason the renewal can not proceed
2659 =cut
2661 sub CanBookBeRenewed {
2662 my ( $borrowernumber, $itemnumber, $override_limit ) = @_;
2664 my $dbh = C4::Context->dbh;
2665 my $renews = 1;
2667 my $item = GetItem($itemnumber) or return ( 0, 'no_item' );
2668 my $itemissue = GetItemIssue($itemnumber) or return ( 0, 'no_checkout' );
2669 return ( 0, 'onsite_checkout' ) if $itemissue->{onsite_checkout};
2671 $borrowernumber ||= $itemissue->{borrowernumber};
2672 my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber )
2673 or return;
2675 my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2677 # This item can fill one or more unfilled reserve, can those unfilled reserves
2678 # all be filled by other available items?
2679 if ( $resfound
2680 && C4::Context->preference('AllowRenewalIfOtherItemsAvailable') )
2682 my $schema = Koha::Database->new()->schema();
2684 my $item_holds = $schema->resultset('Reserve')->search( { itemnumber => $itemnumber, found => undef } )->count();
2685 if ($item_holds) {
2686 # There is an item level hold on this item, no other item can fill the hold
2687 $resfound = 1;
2689 else {
2691 # Get all other items that could possibly fill reserves
2692 my @itemnumbers = $schema->resultset('Item')->search(
2694 biblionumber => $resrec->{biblionumber},
2695 onloan => undef,
2696 notforloan => 0,
2697 -not => { itemnumber => $itemnumber }
2699 { columns => 'itemnumber' }
2700 )->get_column('itemnumber')->all();
2702 # Get all other reserves that could have been filled by this item
2703 my @borrowernumbers;
2704 while (1) {
2705 my ( $reserve_found, $reserve, undef ) =
2706 C4::Reserves::CheckReserves( $itemnumber, undef, undef, \@borrowernumbers );
2708 if ($reserve_found) {
2709 push( @borrowernumbers, $reserve->{borrowernumber} );
2711 else {
2712 last;
2716 # If the count of the union of the lists of reservable items for each borrower
2717 # is equal or greater than the number of borrowers, we know that all reserves
2718 # can be filled with available items. We can get the union of the sets simply
2719 # by pushing all the elements onto an array and removing the duplicates.
2720 my @reservable;
2721 foreach my $b (@borrowernumbers) {
2722 my ($borr) = C4::Members::GetMember( borrowernumber => $b);
2723 foreach my $i (@itemnumbers) {
2724 my $item = GetItem($i);
2725 if ( !IsItemOnHoldAndFound($i)
2726 && IsAvailableForItemLevelRequest( $item, $borr )
2727 && CanItemBeReserved( $b, $i ) )
2729 push( @reservable, $i );
2734 @reservable = uniq(@reservable);
2736 if ( @reservable >= @borrowernumbers ) {
2737 $resfound = 0;
2741 return ( 0, "on_reserve" ) if $resfound; # '' when no hold was found
2743 return ( 1, undef ) if $override_limit;
2745 my $branchcode = _GetCircControlBranch( $item, $borrower );
2746 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
2747 { categorycode => $borrower->{categorycode},
2748 itemtype => $item->{itype},
2749 branchcode => $branchcode
2753 return ( 0, "too_many" )
2754 if not $issuing_rule or $issuing_rule->renewalsallowed <= $itemissue->{renewals};
2756 my $overduesblockrenewing = C4::Context->preference('OverduesBlockRenewing');
2757 my $restrictionblockrenewing = C4::Context->preference('RestrictionBlockRenewing');
2758 my $patron = Koha::Patrons->find($borrowernumber);
2759 my $restricted = $patron->is_debarred;
2760 my $hasoverdues = $patron->has_overdues;
2762 if ( $restricted and $restrictionblockrenewing ) {
2763 return ( 0, 'restriction');
2764 } elsif ( ($hasoverdues and $overduesblockrenewing eq 'block') || ($itemissue->{overdue} and $overduesblockrenewing eq 'blockitem') ) {
2765 return ( 0, 'overdue');
2768 if ( $itemissue->{auto_renew}
2769 and defined $issuing_rule->no_auto_renewal_after
2770 and $issuing_rule->no_auto_renewal_after ne "" ) {
2772 # Get issue_date and add no_auto_renewal_after
2773 # If this is greater than today, it's too late for renewal.
2774 my $maximum_renewal_date = dt_from_string($itemissue->{issuedate});
2775 $maximum_renewal_date->add(
2776 $issuing_rule->lengthunit => $issuing_rule->no_auto_renewal_after
2778 my $now = dt_from_string;
2779 if ( $now >= $maximum_renewal_date ) {
2780 return ( 0, "auto_too_late" );
2784 if ( defined $issuing_rule->norenewalbefore
2785 and $issuing_rule->norenewalbefore ne "" )
2788 # Calculate soonest renewal by subtracting 'No renewal before' from due date
2789 my $soonestrenewal =
2790 $itemissue->{date_due}->clone()
2791 ->subtract(
2792 $issuing_rule->lengthunit => $issuing_rule->norenewalbefore );
2794 # Depending on syspref reset the exact time, only check the date
2795 if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
2796 and $issuing_rule->lengthunit eq 'days' )
2798 $soonestrenewal->truncate( to => 'day' );
2801 if ( $soonestrenewal > DateTime->now( time_zone => C4::Context->tz() ) )
2803 return ( 0, "auto_too_soon" ) if $itemissue->{auto_renew};
2804 return ( 0, "too_soon" );
2806 elsif ( $itemissue->{auto_renew} ) {
2807 return ( 0, "auto_renew" );
2811 # Fallback for automatic renewals:
2812 # If norenewalbefore is undef, don't renew before due date.
2813 if ( $itemissue->{auto_renew} ) {
2814 my $now = dt_from_string;
2815 return ( 0, "auto_renew" )
2816 if $now >= $itemissue->{date_due};
2817 return ( 0, "auto_too_soon" );
2820 return ( 1, undef );
2823 =head2 AddRenewal
2825 &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
2827 Renews a loan.
2829 C<$borrowernumber> is the borrower number of the patron who currently
2830 has the item.
2832 C<$itemnumber> is the number of the item to renew.
2834 C<$branch> is the library where the renewal took place (if any).
2835 The library that controls the circ policies for the renewal is retrieved from the issues record.
2837 C<$datedue> can be a DateTime object used to set the due date.
2839 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate. If
2840 this parameter is not supplied, lastreneweddate is set to the current date.
2842 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2843 from the book's item type.
2845 =cut
2847 sub AddRenewal {
2848 my $borrowernumber = shift;
2849 my $itemnumber = shift or return;
2850 my $branch = shift;
2851 my $datedue = shift;
2852 my $lastreneweddate = shift || DateTime->now(time_zone => C4::Context->tz)->ymd();
2854 my $item = GetItem($itemnumber) or return;
2855 my $biblio = GetBiblioFromItemNumber($itemnumber) or return;
2857 my $dbh = C4::Context->dbh;
2859 # Find the issues record for this book
2860 my $issuedata = GetItemIssue($itemnumber);
2862 return unless ( $issuedata );
2864 $borrowernumber ||= $issuedata->{borrowernumber};
2866 if ( defined $datedue && ref $datedue ne 'DateTime' ) {
2867 carp 'Invalid date passed to AddRenewal.';
2868 return;
2871 my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber ) or return;
2873 if ( C4::Context->preference('CalculateFinesOnReturn') && $issuedata->{overdue} ) {
2874 _CalculateAndUpdateFine( { issue => $issuedata, item => $item, borrower => $borrower } );
2876 _FixOverduesOnReturn( $borrowernumber, $itemnumber );
2878 # If the due date wasn't specified, calculate it by adding the
2879 # book's loan length to today's date or the current due date
2880 # based on the value of the RenewalPeriodBase syspref.
2881 unless ($datedue) {
2883 my $itemtype = (C4::Context->preference('item-level_itypes')) ? $biblio->{'itype'} : $biblio->{'itemtype'};
2885 $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
2886 dt_from_string( $issuedata->{date_due} ) :
2887 DateTime->now( time_zone => C4::Context->tz());
2888 $datedue = CalcDateDue($datedue, $itemtype, $issuedata->{'branchcode'}, $borrower, 'is a renewal');
2891 # Update the issues record to have the new due date, and a new count
2892 # of how many times it has been renewed.
2893 my $renews = $issuedata->{'renewals'} + 1;
2894 my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
2895 WHERE borrowernumber=?
2896 AND itemnumber=?"
2899 $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
2901 # Update the renewal count on the item, and tell zebra to reindex
2902 $renews = $biblio->{'renewals'} + 1;
2903 ModItem({ renewals => $renews, onloan => $datedue->strftime('%Y-%m-%d %H:%M')}, $biblio->{'biblionumber'}, $itemnumber);
2905 # Charge a new rental fee, if applicable?
2906 my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
2907 if ( $charge > 0 ) {
2908 my $accountno = getnextacctno( $borrowernumber );
2909 my $item = GetBiblioFromItemNumber($itemnumber);
2910 my $manager_id = 0;
2911 $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
2912 $sth = $dbh->prepare(
2913 "INSERT INTO accountlines
2914 (date, borrowernumber, accountno, amount, manager_id,
2915 description,accounttype, amountoutstanding, itemnumber)
2916 VALUES (now(),?,?,?,?,?,?,?,?)"
2918 $sth->execute( $borrowernumber, $accountno, $charge, $manager_id,
2919 "Renewal of Rental Item $item->{'title'} $item->{'barcode'}",
2920 'Rent', $charge, $itemnumber );
2923 # Send a renewal slip according to checkout alert preferencei
2924 if ( C4::Context->preference('RenewalSendNotice') eq '1' ) {
2925 $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber );
2926 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2927 my %conditions = (
2928 branchcode => $branch,
2929 categorycode => $borrower->{categorycode},
2930 item_type => $item->{itype},
2931 notification => 'CHECKOUT',
2933 if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
2934 SendCirculationAlert(
2936 type => 'RENEWAL',
2937 item => $item,
2938 borrower => $borrower,
2939 branch => $branch,
2945 # Remove any OVERDUES related debarment if the borrower has no overdues
2946 $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber );
2947 if ( $borrowernumber
2948 && $borrower->{'debarred'}
2949 && !Koha::Patrons->find( $borrowernumber )->has_overdues
2950 && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2952 DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2955 # Log the renewal
2956 UpdateStats(
2958 branch => C4::Context->userenv ? C4::Context->userenv->{branch} : $branch,
2959 type => 'renew',
2960 amount => $charge,
2961 itemnumber => $itemnumber,
2962 itemtype => $item->{itype},
2963 borrowernumber => $borrowernumber,
2964 ccode => $item->{'ccode'}
2968 return $datedue;
2971 sub GetRenewCount {
2972 # check renewal status
2973 my ( $bornum, $itemno ) = @_;
2974 my $dbh = C4::Context->dbh;
2975 my $renewcount = 0;
2976 my $renewsallowed = 0;
2977 my $renewsleft = 0;
2979 my $borrower = C4::Members::GetMember( borrowernumber => $bornum);
2980 my $item = GetItem($itemno);
2982 # Look in the issues table for this item, lent to this borrower,
2983 # and not yet returned.
2985 # FIXME - I think this function could be redone to use only one SQL call.
2986 my $sth = $dbh->prepare(
2987 "select * from issues
2988 where (borrowernumber = ?)
2989 and (itemnumber = ?)"
2991 $sth->execute( $bornum, $itemno );
2992 my $data = $sth->fetchrow_hashref;
2993 $renewcount = $data->{'renewals'} if $data->{'renewals'};
2994 # $item and $borrower should be calculated
2995 my $branchcode = _GetCircControlBranch($item, $borrower);
2997 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
2998 { categorycode => $borrower->{categorycode},
2999 itemtype => $item->{itype},
3000 branchcode => $branchcode
3004 $renewsallowed = $issuing_rule ? $issuing_rule->renewalsallowed : undef; # FIXME Just replace undef with 0 to get what we expected. But what about the side-effects? TODO LATER
3005 $renewsleft = $renewsallowed - $renewcount;
3006 if($renewsleft < 0){ $renewsleft = 0; }
3007 return ( $renewcount, $renewsallowed, $renewsleft );
3010 =head2 GetSoonestRenewDate
3012 $NoRenewalBeforeThisDate = &GetSoonestRenewDate($borrowernumber, $itemnumber);
3014 Find out the soonest possible renew date of a borrowed item.
3016 C<$borrowernumber> is the borrower number of the patron who currently
3017 has the item on loan.
3019 C<$itemnumber> is the number of the item to renew.
3021 C<$GetSoonestRenewDate> returns the DateTime of the soonest possible
3022 renew date, based on the value "No renewal before" of the applicable
3023 issuing rule. Returns the current date if the item can already be
3024 renewed, and returns undefined if the borrower, loan, or item
3025 cannot be found.
3027 =cut
3029 sub GetSoonestRenewDate {
3030 my ( $borrowernumber, $itemnumber ) = @_;
3032 my $dbh = C4::Context->dbh;
3034 my $item = GetItem($itemnumber) or return;
3035 my $itemissue = GetItemIssue($itemnumber) or return;
3037 $borrowernumber ||= $itemissue->{borrowernumber};
3038 my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber )
3039 or return;
3041 my $branchcode = _GetCircControlBranch( $item, $borrower );
3042 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
3043 { categorycode => $borrower->{categorycode},
3044 itemtype => $item->{itype},
3045 branchcode => $branchcode
3049 my $now = dt_from_string;
3050 return $now unless $issuing_rule;
3052 if ( defined $issuing_rule->norenewalbefore
3053 and $issuing_rule->norenewalbefore ne "" )
3055 my $soonestrenewal =
3056 $itemissue->{date_due}->clone()
3057 ->subtract(
3058 $issuing_rule->lengthunit => $issuing_rule->norenewalbefore );
3060 if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
3061 and $issuing_rule->lengthunit eq 'days' )
3063 $soonestrenewal->truncate( to => 'day' );
3065 return $soonestrenewal if $now < $soonestrenewal;
3067 return $now;
3070 =head2 GetLatestAutoRenewDate
3072 $NoAutoRenewalAfterThisDate = &GetLatestAutoRenewDate($borrowernumber, $itemnumber);
3074 Find out the latest possible auto renew date of a borrowed item.
3076 C<$borrowernumber> is the borrower number of the patron who currently
3077 has the item on loan.
3079 C<$itemnumber> is the number of the item to renew.
3081 C<$GetLatestAutoRenewDate> returns the DateTime of the latest possible
3082 auto renew date, based on the value "No auto renewal after" of the applicable
3083 issuing rule.
3084 Returns undef if there is no date specify in the circ rules or if the patron, loan,
3085 or item cannot be found.
3087 =cut
3089 sub GetLatestAutoRenewDate {
3090 my ( $borrowernumber, $itemnumber ) = @_;
3092 my $dbh = C4::Context->dbh;
3094 my $item = GetItem($itemnumber) or return;
3095 my $itemissue = GetItemIssue($itemnumber) or return;
3097 $borrowernumber ||= $itemissue->{borrowernumber};
3098 my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber )
3099 or return;
3101 my $branchcode = _GetCircControlBranch( $item, $borrower );
3102 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
3103 { categorycode => $borrower->{categorycode},
3104 itemtype => $item->{itype},
3105 branchcode => $branchcode
3109 return unless $issuing_rule;
3110 return if not $issuing_rule->no_auto_renewal_after
3111 or $issuing_rule->no_auto_renewal_after eq '';
3113 my $maximum_renewal_date = dt_from_string($itemissue->{issuedate});
3114 $maximum_renewal_date->add(
3115 $issuing_rule->lengthunit => $issuing_rule->no_auto_renewal_after
3118 return $maximum_renewal_date;
3122 =head2 GetIssuingCharges
3124 ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
3126 Calculate how much it would cost for a given patron to borrow a given
3127 item, including any applicable discounts.
3129 C<$itemnumber> is the item number of item the patron wishes to borrow.
3131 C<$borrowernumber> is the patron's borrower number.
3133 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
3134 and C<$item_type> is the code for the item's item type (e.g., C<VID>
3135 if it's a video).
3137 =cut
3139 sub GetIssuingCharges {
3141 # calculate charges due
3142 my ( $itemnumber, $borrowernumber ) = @_;
3143 my $charge = 0;
3144 my $dbh = C4::Context->dbh;
3145 my $item_type;
3147 # Get the book's item type and rental charge (via its biblioitem).
3148 my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
3149 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
3150 $charge_query .= (C4::Context->preference('item-level_itypes'))
3151 ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
3152 : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
3154 $charge_query .= ' WHERE items.itemnumber =?';
3156 my $sth = $dbh->prepare($charge_query);
3157 $sth->execute($itemnumber);
3158 if ( my $item_data = $sth->fetchrow_hashref ) {
3159 $item_type = $item_data->{itemtype};
3160 $charge = $item_data->{rentalcharge};
3161 my $branch = C4::Context::mybranch();
3162 my $discount_query = q|SELECT rentaldiscount,
3163 issuingrules.itemtype, issuingrules.branchcode
3164 FROM borrowers
3165 LEFT JOIN issuingrules ON borrowers.categorycode = issuingrules.categorycode
3166 WHERE borrowers.borrowernumber = ?
3167 AND (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
3168 AND (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')|;
3169 my $discount_sth = $dbh->prepare($discount_query);
3170 $discount_sth->execute( $borrowernumber, $item_type, $branch );
3171 my $discount_rules = $discount_sth->fetchall_arrayref({});
3172 if (@{$discount_rules}) {
3173 # We may have multiple rules so get the most specific
3174 my $discount = _get_discount_from_rule($discount_rules, $branch, $item_type);
3175 $charge = ( $charge * ( 100 - $discount ) ) / 100;
3179 return ( $charge, $item_type );
3182 # Select most appropriate discount rule from those returned
3183 sub _get_discount_from_rule {
3184 my ($rules_ref, $branch, $itemtype) = @_;
3185 my $discount;
3187 if (@{$rules_ref} == 1) { # only 1 applicable rule use it
3188 $discount = $rules_ref->[0]->{rentaldiscount};
3189 return (defined $discount) ? $discount : 0;
3191 # could have up to 4 does one match $branch and $itemtype
3192 my @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq $itemtype } @{$rules_ref};
3193 if (@d) {
3194 $discount = $d[0]->{rentaldiscount};
3195 return (defined $discount) ? $discount : 0;
3197 # do we have item type + all branches
3198 @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq $itemtype } @{$rules_ref};
3199 if (@d) {
3200 $discount = $d[0]->{rentaldiscount};
3201 return (defined $discount) ? $discount : 0;
3203 # do we all item types + this branch
3204 @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq q{*} } @{$rules_ref};
3205 if (@d) {
3206 $discount = $d[0]->{rentaldiscount};
3207 return (defined $discount) ? $discount : 0;
3209 # so all and all (surely we wont get here)
3210 @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq q{*} } @{$rules_ref};
3211 if (@d) {
3212 $discount = $d[0]->{rentaldiscount};
3213 return (defined $discount) ? $discount : 0;
3215 # none of the above
3216 return 0;
3219 =head2 AddIssuingCharge
3221 &AddIssuingCharge( $itemno, $borrowernumber, $charge )
3223 =cut
3225 sub AddIssuingCharge {
3226 my ( $itemnumber, $borrowernumber, $charge ) = @_;
3227 my $dbh = C4::Context->dbh;
3228 my $nextaccntno = getnextacctno( $borrowernumber );
3229 my $manager_id = 0;
3230 $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
3231 my $query ="
3232 INSERT INTO accountlines
3233 (borrowernumber, itemnumber, accountno,
3234 date, amount, description, accounttype,
3235 amountoutstanding, manager_id)
3236 VALUES (?, ?, ?,now(), ?, 'Rental', 'Rent',?,?)
3238 my $sth = $dbh->prepare($query);
3239 $sth->execute( $borrowernumber, $itemnumber, $nextaccntno, $charge, $charge, $manager_id );
3242 =head2 GetTransfers
3244 GetTransfers($itemnumber);
3246 =cut
3248 sub GetTransfers {
3249 my ($itemnumber) = @_;
3251 my $dbh = C4::Context->dbh;
3253 my $query = '
3254 SELECT datesent,
3255 frombranch,
3256 tobranch
3257 FROM branchtransfers
3258 WHERE itemnumber = ?
3259 AND datearrived IS NULL
3261 my $sth = $dbh->prepare($query);
3262 $sth->execute($itemnumber);
3263 my @row = $sth->fetchrow_array();
3264 return @row;
3267 =head2 GetTransfersFromTo
3269 @results = GetTransfersFromTo($frombranch,$tobranch);
3271 Returns the list of pending transfers between $from and $to branch
3273 =cut
3275 sub GetTransfersFromTo {
3276 my ( $frombranch, $tobranch ) = @_;
3277 return unless ( $frombranch && $tobranch );
3278 my $dbh = C4::Context->dbh;
3279 my $query = "
3280 SELECT itemnumber,datesent,frombranch
3281 FROM branchtransfers
3282 WHERE frombranch=?
3283 AND tobranch=?
3284 AND datearrived IS NULL
3286 my $sth = $dbh->prepare($query);
3287 $sth->execute( $frombranch, $tobranch );
3288 my @gettransfers;
3290 while ( my $data = $sth->fetchrow_hashref ) {
3291 push @gettransfers, $data;
3293 return (@gettransfers);
3296 =head2 DeleteTransfer
3298 &DeleteTransfer($itemnumber);
3300 =cut
3302 sub DeleteTransfer {
3303 my ($itemnumber) = @_;
3304 return unless $itemnumber;
3305 my $dbh = C4::Context->dbh;
3306 my $sth = $dbh->prepare(
3307 "DELETE FROM branchtransfers
3308 WHERE itemnumber=?
3309 AND datearrived IS NULL "
3311 return $sth->execute($itemnumber);
3314 =head2 AnonymiseIssueHistory
3316 ($rows,$err_history_not_deleted) = AnonymiseIssueHistory($date,$borrowernumber)
3318 This function write NULL instead of C<$borrowernumber> given on input arg into the table issues.
3319 if C<$borrowernumber> is not set, it will delete the issue history for all borrower older than C<$date>.
3321 If c<$borrowernumber> is set, it will delete issue history for only that borrower, regardless of their opac privacy
3322 setting (force delete).
3324 return the number of affected rows and a value that evaluates to true if an error occurred deleting the history.
3326 =cut
3328 sub AnonymiseIssueHistory {
3329 my $date = shift;
3330 my $borrowernumber = shift;
3331 my $dbh = C4::Context->dbh;
3332 my $query = "
3333 UPDATE old_issues
3334 SET borrowernumber = ?
3335 WHERE returndate < ?
3336 AND borrowernumber IS NOT NULL
3339 # The default of 0 does not work due to foreign key constraints
3340 # The anonymisation should not fail quietly if AnonymousPatron is not a valid entry
3341 # Set it to undef (NULL)
3342 my $anonymouspatron = C4::Context->preference('AnonymousPatron') || undef;
3343 my @bind_params = ($anonymouspatron, $date);
3344 if (defined $borrowernumber) {
3345 $query .= " AND borrowernumber = ?";
3346 push @bind_params, $borrowernumber;
3347 } else {
3348 $query .= " AND (SELECT privacy FROM borrowers WHERE borrowers.borrowernumber=old_issues.borrowernumber) <> 0";
3350 my $sth = $dbh->prepare($query);
3351 $sth->execute(@bind_params);
3352 my $anonymisation_err = $dbh->err;
3353 my $rows_affected = $sth->rows; ### doublecheck row count return function
3354 return ($rows_affected, $anonymisation_err);
3357 =head2 SendCirculationAlert
3359 Send out a C<check-in> or C<checkout> alert using the messaging system.
3361 B<Parameters>:
3363 =over 4
3365 =item type
3367 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
3369 =item item
3371 Hashref of information about the item being checked in or out.
3373 =item borrower
3375 Hashref of information about the borrower of the item.
3377 =item branch
3379 The branchcode from where the checkout or check-in took place.
3381 =back
3383 B<Example>:
3385 SendCirculationAlert({
3386 type => 'CHECKOUT',
3387 item => $item,
3388 borrower => $borrower,
3389 branch => $branch,
3392 =cut
3394 sub SendCirculationAlert {
3395 my ($opts) = @_;
3396 my ($type, $item, $borrower, $branch) =
3397 ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
3398 my %message_name = (
3399 CHECKIN => 'Item_Check_in',
3400 CHECKOUT => 'Item_Checkout',
3401 RENEWAL => 'Item_Checkout',
3403 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
3404 borrowernumber => $borrower->{borrowernumber},
3405 message_name => $message_name{$type},
3407 my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues';
3409 my @transports = keys %{ $borrower_preferences->{transports} };
3410 # warn "no transports" unless @transports;
3411 for (@transports) {
3412 # warn "transport: $_";
3413 my $message = C4::Message->find_last_message($borrower, $type, $_);
3414 if (!$message) {
3415 #warn "create new message";
3416 my $letter = C4::Letters::GetPreparedLetter (
3417 module => 'circulation',
3418 letter_code => $type,
3419 branchcode => $branch,
3420 message_transport_type => $_,
3421 tables => {
3422 $issues_table => $item->{itemnumber},
3423 'items' => $item->{itemnumber},
3424 'biblio' => $item->{biblionumber},
3425 'biblioitems' => $item->{biblionumber},
3426 'borrowers' => $borrower,
3427 'branches' => $branch,
3429 ) or next;
3430 C4::Message->enqueue($letter, $borrower, $_);
3431 } else {
3432 #warn "append to old message";
3433 my $letter = C4::Letters::GetPreparedLetter (
3434 module => 'circulation',
3435 letter_code => $type,
3436 branchcode => $branch,
3437 message_transport_type => $_,
3438 tables => {
3439 $issues_table => $item->{itemnumber},
3440 'items' => $item->{itemnumber},
3441 'biblio' => $item->{biblionumber},
3442 'biblioitems' => $item->{biblionumber},
3443 'borrowers' => $borrower,
3444 'branches' => $branch,
3446 ) or next;
3447 $message->append($letter);
3448 $message->update;
3452 return;
3455 =head2 updateWrongTransfer
3457 $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
3459 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
3461 =cut
3463 sub updateWrongTransfer {
3464 my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
3465 my $dbh = C4::Context->dbh;
3466 # first step validate the actual line of transfert .
3467 my $sth =
3468 $dbh->prepare(
3469 "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
3471 $sth->execute($FromLibrary,$itemNumber);
3473 # second step create a new line of branchtransfer to the right location .
3474 ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
3476 #third step changing holdingbranch of item
3477 UpdateHoldingbranch($FromLibrary,$itemNumber);
3480 =head2 UpdateHoldingbranch
3482 $items = UpdateHoldingbranch($branch,$itmenumber);
3484 Simple methode for updating hodlingbranch in items BDD line
3486 =cut
3488 sub UpdateHoldingbranch {
3489 my ( $branch,$itemnumber ) = @_;
3490 ModItem({ holdingbranch => $branch }, undef, $itemnumber);
3493 =head2 CalcDateDue
3495 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
3497 this function calculates the due date given the start date and configured circulation rules,
3498 checking against the holidays calendar as per the 'useDaysMode' syspref.
3499 C<$startdate> = DateTime object representing start date of loan period (assumed to be today)
3500 C<$itemtype> = itemtype code of item in question
3501 C<$branch> = location whose calendar to use
3502 C<$borrower> = Borrower object
3503 C<$isrenewal> = Boolean: is true if we want to calculate the date due for a renewal. Else is false.
3505 =cut
3507 sub CalcDateDue {
3508 my ( $startdate, $itemtype, $branch, $borrower, $isrenewal ) = @_;
3510 $isrenewal ||= 0;
3512 # loanlength now a href
3513 my $loanlength =
3514 GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
3516 my $length_key = ( $isrenewal and defined $loanlength->{renewalperiod} )
3517 ? qq{renewalperiod}
3518 : qq{issuelength};
3520 my $datedue;
3521 if ( $startdate ) {
3522 if (ref $startdate ne 'DateTime' ) {
3523 $datedue = dt_from_string($datedue);
3524 } else {
3525 $datedue = $startdate->clone;
3527 } else {
3528 $datedue =
3529 DateTime->now( time_zone => C4::Context->tz() )
3530 ->truncate( to => 'minute' );
3534 # calculate the datedue as normal
3535 if ( C4::Context->preference('useDaysMode') eq 'Days' )
3536 { # ignoring calendar
3537 if ( $loanlength->{lengthunit} eq 'hours' ) {
3538 $datedue->add( hours => $loanlength->{$length_key} );
3539 } else { # days
3540 $datedue->add( days => $loanlength->{$length_key} );
3541 $datedue->set_hour(23);
3542 $datedue->set_minute(59);
3544 } else {
3545 my $dur;
3546 if ($loanlength->{lengthunit} eq 'hours') {
3547 $dur = DateTime::Duration->new( hours => $loanlength->{$length_key});
3549 else { # days
3550 $dur = DateTime::Duration->new( days => $loanlength->{$length_key});
3552 my $calendar = Koha::Calendar->new( branchcode => $branch );
3553 $datedue = $calendar->addDate( $datedue, $dur, $loanlength->{lengthunit} );
3554 if ($loanlength->{lengthunit} eq 'days') {
3555 $datedue->set_hour(23);
3556 $datedue->set_minute(59);
3560 # if Hard Due Dates are used, retrieve them and apply as necessary
3561 my ( $hardduedate, $hardduedatecompare ) =
3562 GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
3563 if ($hardduedate) { # hardduedates are currently dates
3564 $hardduedate->truncate( to => 'minute' );
3565 $hardduedate->set_hour(23);
3566 $hardduedate->set_minute(59);
3567 my $cmp = DateTime->compare( $hardduedate, $datedue );
3569 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
3570 # if the calculated date is before the 'after' Hard Due Date (floor), override
3571 # if the hard due date is set to 'exactly', overrride
3572 if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
3573 $datedue = $hardduedate->clone;
3576 # in all other cases, keep the date due as it is
3580 # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3581 if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3582 my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso', 'floating');
3583 if( $expiry_dt ) { #skip empty expiry date..
3584 $expiry_dt->set( hour => 23, minute => 59);
3585 my $d1= $datedue->clone->set_time_zone('floating');
3586 if ( DateTime->compare( $d1, $expiry_dt ) == 1 ) {
3587 $datedue = $expiry_dt->clone->set_time_zone( C4::Context->tz );
3592 return $datedue;
3596 sub CheckValidBarcode{
3597 my ($barcode) = @_;
3598 my $dbh = C4::Context->dbh;
3599 my $query=qq|SELECT count(*)
3600 FROM items
3601 WHERE barcode=?
3603 my $sth = $dbh->prepare($query);
3604 $sth->execute($barcode);
3605 my $exist=$sth->fetchrow ;
3606 return $exist;
3609 =head2 IsBranchTransferAllowed
3611 $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3613 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3615 =cut
3617 sub IsBranchTransferAllowed {
3618 my ( $toBranch, $fromBranch, $code ) = @_;
3620 if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3622 my $limitType = C4::Context->preference("BranchTransferLimitsType");
3623 my $dbh = C4::Context->dbh;
3625 my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3626 $sth->execute( $toBranch, $fromBranch, $code );
3627 my $limit = $sth->fetchrow_hashref();
3629 ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3630 if ( $limit->{'limitId'} ) {
3631 return 0;
3632 } else {
3633 return 1;
3637 =head2 CreateBranchTransferLimit
3639 CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3641 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3643 =cut
3645 sub CreateBranchTransferLimit {
3646 my ( $toBranch, $fromBranch, $code ) = @_;
3647 return unless defined($toBranch) && defined($fromBranch);
3648 my $limitType = C4::Context->preference("BranchTransferLimitsType");
3650 my $dbh = C4::Context->dbh;
3652 my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3653 return $sth->execute( $code, $toBranch, $fromBranch );
3656 =head2 DeleteBranchTransferLimits
3658 my $result = DeleteBranchTransferLimits($frombranch);
3660 Deletes all the library transfer limits for one library. Returns the
3661 number of limits deleted, 0e0 if no limits were deleted, or undef if
3662 no arguments are supplied.
3664 =cut
3666 sub DeleteBranchTransferLimits {
3667 my $branch = shift;
3668 return unless defined $branch;
3669 my $dbh = C4::Context->dbh;
3670 my $sth = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3671 return $sth->execute($branch);
3674 sub ReturnLostItem{
3675 my ( $borrowernumber, $itemnum ) = @_;
3677 MarkIssueReturned( $borrowernumber, $itemnum );
3678 my $borrower = C4::Members::GetMember( 'borrowernumber'=>$borrowernumber );
3679 my $item = C4::Items::GetItem( $itemnum );
3680 my $old_note = ($item->{'paidfor'} && ($item->{'paidfor'} ne q{})) ? $item->{'paidfor'}.' / ' : q{};
3681 my @datearr = localtime(time);
3682 my $date = ( 1900 + $datearr[5] ) . "-" . ( $datearr[4] + 1 ) . "-" . $datearr[3];
3683 my $bor = "$borrower->{'firstname'} $borrower->{'surname'} $borrower->{'cardnumber'}";
3684 ModItem({ paidfor => $old_note."Paid for by $bor $date" }, undef, $itemnum);
3688 sub LostItem{
3689 my ($itemnumber, $mark_returned) = @_;
3691 my $dbh = C4::Context->dbh();
3692 my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title
3693 FROM issues
3694 JOIN items USING (itemnumber)
3695 JOIN biblio USING (biblionumber)
3696 WHERE issues.itemnumber=?");
3697 $sth->execute($itemnumber);
3698 my $issues=$sth->fetchrow_hashref();
3700 # If a borrower lost the item, add a replacement cost to the their record
3701 if ( my $borrowernumber = $issues->{borrowernumber} ){
3702 my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber );
3704 if (C4::Context->preference('WhenLostForgiveFine')){
3705 my $fix = _FixOverduesOnReturn($borrowernumber, $itemnumber, 1, 0); # 1, 0 = exemptfine, no-dropbox
3706 defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $itemnumber...) failed!"; # zero is OK, check defined
3708 if (C4::Context->preference('WhenLostChargeReplacementFee')){
3709 C4::Accounts::chargelostitem($borrowernumber, $itemnumber, $issues->{'replacementprice'}, "Lost Item $issues->{'title'} $issues->{'barcode'}");
3710 #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3711 #warn " $issues->{'borrowernumber'} / $itemnumber ";
3714 MarkIssueReturned($borrowernumber,$itemnumber,undef,undef,$borrower->{'privacy'}) if $mark_returned;
3718 sub GetOfflineOperations {
3719 my $dbh = C4::Context->dbh;
3720 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3721 $sth->execute(C4::Context->userenv->{'branch'});
3722 my $results = $sth->fetchall_arrayref({});
3723 return $results;
3726 sub GetOfflineOperation {
3727 my $operationid = shift;
3728 return unless $operationid;
3729 my $dbh = C4::Context->dbh;
3730 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3731 $sth->execute( $operationid );
3732 return $sth->fetchrow_hashref;
3735 sub AddOfflineOperation {
3736 my ( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount ) = @_;
3737 my $dbh = C4::Context->dbh;
3738 my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber, amount) VALUES(?,?,?,?,?,?,?)");
3739 $sth->execute( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount );
3740 return "Added.";
3743 sub DeleteOfflineOperation {
3744 my $dbh = C4::Context->dbh;
3745 my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3746 $sth->execute( shift );
3747 return "Deleted.";
3750 sub ProcessOfflineOperation {
3751 my $operation = shift;
3753 my $report;
3754 if ( $operation->{action} eq 'return' ) {
3755 $report = ProcessOfflineReturn( $operation );
3756 } elsif ( $operation->{action} eq 'issue' ) {
3757 $report = ProcessOfflineIssue( $operation );
3758 } elsif ( $operation->{action} eq 'payment' ) {
3759 $report = ProcessOfflinePayment( $operation );
3762 DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3764 return $report;
3767 sub ProcessOfflineReturn {
3768 my $operation = shift;
3770 my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3772 if ( $itemnumber ) {
3773 my $issue = GetOpenIssue( $itemnumber );
3774 if ( $issue ) {
3775 MarkIssueReturned(
3776 $issue->{borrowernumber},
3777 $itemnumber,
3778 undef,
3779 $operation->{timestamp},
3781 ModItem(
3782 { renewals => 0, onloan => undef },
3783 $issue->{'biblionumber'},
3784 $itemnumber
3786 return "Success.";
3787 } else {
3788 return "Item not issued.";
3790 } else {
3791 return "Item not found.";
3795 sub ProcessOfflineIssue {
3796 my $operation = shift;
3798 my $borrower = C4::Members::GetMember( cardnumber => $operation->{cardnumber} );
3800 if ( $borrower->{borrowernumber} ) {
3801 my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3802 unless ($itemnumber) {
3803 return "Barcode not found.";
3805 my $issue = GetOpenIssue( $itemnumber );
3807 if ( $issue and ( $issue->{borrowernumber} ne $borrower->{borrowernumber} ) ) { # Item already issued to another borrower, mark it returned
3808 MarkIssueReturned(
3809 $issue->{borrowernumber},
3810 $itemnumber,
3811 undef,
3812 $operation->{timestamp},
3815 AddIssue(
3816 $borrower,
3817 $operation->{'barcode'},
3818 undef,
3820 $operation->{timestamp},
3821 undef,
3823 return "Success.";
3824 } else {
3825 return "Borrower not found.";
3829 sub ProcessOfflinePayment {
3830 my $operation = shift;
3832 my $patron = Koha::Patrons->find( { cardnumber => $operation->{cardnumber} });
3833 my $amount = $operation->{amount};
3835 Koha::Account->new( { patron_id => $patron->id } )->pay( { amount => $amount } );
3837 return "Success."
3841 =head2 TransferSlip
3843 TransferSlip($user_branch, $itemnumber, $barcode, $to_branch)
3845 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
3847 =cut
3849 sub TransferSlip {
3850 my ($branch, $itemnumber, $barcode, $to_branch) = @_;
3852 my $item = GetItem( $itemnumber, $barcode )
3853 or return;
3855 return C4::Letters::GetPreparedLetter (
3856 module => 'circulation',
3857 letter_code => 'TRANSFERSLIP',
3858 branchcode => $branch,
3859 tables => {
3860 'branches' => $to_branch,
3861 'biblio' => $item->{biblionumber},
3862 'items' => $item,
3867 =head2 CheckIfIssuedToPatron
3869 CheckIfIssuedToPatron($borrowernumber, $biblionumber)
3871 Return 1 if any record item is issued to patron, otherwise return 0
3873 =cut
3875 sub CheckIfIssuedToPatron {
3876 my ($borrowernumber, $biblionumber) = @_;
3878 my $dbh = C4::Context->dbh;
3879 my $query = q|
3880 SELECT COUNT(*) FROM issues
3881 LEFT JOIN items ON items.itemnumber = issues.itemnumber
3882 WHERE items.biblionumber = ?
3883 AND issues.borrowernumber = ?
3885 my $is_issued = $dbh->selectrow_array($query, {}, $biblionumber, $borrowernumber );
3886 return 1 if $is_issued;
3887 return;
3890 =head2 IsItemIssued
3892 IsItemIssued( $itemnumber )
3894 Return 1 if the item is on loan, otherwise return 0
3896 =cut
3898 sub IsItemIssued {
3899 my $itemnumber = shift;
3900 my $dbh = C4::Context->dbh;
3901 my $sth = $dbh->prepare(q{
3902 SELECT COUNT(*)
3903 FROM issues
3904 WHERE itemnumber = ?
3906 $sth->execute($itemnumber);
3907 return $sth->fetchrow;
3910 =head2 GetAgeRestriction
3912 my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions, $borrower);
3913 my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions);
3915 if($daysToAgeRestriction <= 0) { #Borrower is allowed to access this material, as he is older or as old as the agerestriction }
3916 if($daysToAgeRestriction > 0) { #Borrower is this many days from meeting the agerestriction }
3918 @PARAM1 the koha.biblioitems.agerestriction value, like K18, PEGI 13, ...
3919 @PARAM2 a borrower-object with koha.borrowers.dateofbirth. (OPTIONAL)
3920 @RETURNS The age restriction age in years and the days to fulfill the age restriction for the given borrower.
3921 Negative days mean the borrower has gone past the age restriction age.
3923 =cut
3925 sub GetAgeRestriction {
3926 my ($record_restrictions, $borrower) = @_;
3927 my $markers = C4::Context->preference('AgeRestrictionMarker');
3929 # Split $record_restrictions to something like FSK 16 or PEGI 6
3930 my @values = split ' ', uc($record_restrictions);
3931 return unless @values;
3933 # Search first occurrence of one of the markers
3934 my @markers = split /\|/, uc($markers);
3935 return unless @markers;
3937 my $index = 0;
3938 my $restriction_year = 0;
3939 for my $value (@values) {
3940 $index++;
3941 for my $marker (@markers) {
3942 $marker =~ s/^\s+//; #remove leading spaces
3943 $marker =~ s/\s+$//; #remove trailing spaces
3944 if ( $marker eq $value ) {
3945 if ( $index <= $#values ) {
3946 $restriction_year += $values[$index];
3948 last;
3950 elsif ( $value =~ /^\Q$marker\E(\d+)$/ ) {
3952 # Perhaps it is something like "K16" (as in Finland)
3953 $restriction_year += $1;
3954 last;
3957 last if ( $restriction_year > 0 );
3960 #Check if the borrower is age restricted for this material and for how long.
3961 if ($restriction_year && $borrower) {
3962 if ( $borrower->{'dateofbirth'} ) {
3963 my @alloweddate = split /-/, $borrower->{'dateofbirth'};
3964 $alloweddate[0] += $restriction_year;
3966 #Prevent runime eror on leap year (invalid date)
3967 if ( ( $alloweddate[1] == 2 ) && ( $alloweddate[2] == 29 ) ) {
3968 $alloweddate[2] = 28;
3971 #Get how many days the borrower has to reach the age restriction
3972 my @Today = split /-/, DateTime->today->ymd();
3973 my $daysToAgeRestriction = Date_to_Days(@alloweddate) - Date_to_Days(@Today);
3974 #Negative days means the borrower went past the age restriction age
3975 return ($restriction_year, $daysToAgeRestriction);
3979 return ($restriction_year);
3983 =head2 GetPendingOnSiteCheckouts
3985 =cut
3987 sub GetPendingOnSiteCheckouts {
3988 my $dbh = C4::Context->dbh;
3989 return $dbh->selectall_arrayref(q|
3990 SELECT
3991 items.barcode,
3992 items.biblionumber,
3993 items.itemnumber,
3994 items.itemnotes,
3995 items.itemcallnumber,
3996 items.location,
3997 issues.date_due,
3998 issues.branchcode,
3999 issues.date_due < NOW() AS is_overdue,
4000 biblio.author,
4001 biblio.title,
4002 borrowers.firstname,
4003 borrowers.surname,
4004 borrowers.cardnumber,
4005 borrowers.borrowernumber
4006 FROM items
4007 LEFT JOIN issues ON items.itemnumber = issues.itemnumber
4008 LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
4009 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
4010 WHERE issues.onsite_checkout = 1
4011 |, { Slice => {} } );
4014 sub GetTopIssues {
4015 my ($params) = @_;
4017 my ($count, $branch, $itemtype, $ccode, $newness)
4018 = @$params{qw(count branch itemtype ccode newness)};
4020 my $dbh = C4::Context->dbh;
4021 my $query = q{
4022 SELECT b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4023 bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4024 i.ccode, SUM(i.issues) AS count
4025 FROM biblio b
4026 LEFT JOIN items i ON (i.biblionumber = b.biblionumber)
4027 LEFT JOIN biblioitems bi ON (bi.biblionumber = b.biblionumber)
4030 my (@where_strs, @where_args);
4032 if ($branch) {
4033 push @where_strs, 'i.homebranch = ?';
4034 push @where_args, $branch;
4036 if ($itemtype) {
4037 if (C4::Context->preference('item-level_itypes')){
4038 push @where_strs, 'i.itype = ?';
4039 push @where_args, $itemtype;
4040 } else {
4041 push @where_strs, 'bi.itemtype = ?';
4042 push @where_args, $itemtype;
4045 if ($ccode) {
4046 push @where_strs, 'i.ccode = ?';
4047 push @where_args, $ccode;
4049 if ($newness) {
4050 push @where_strs, 'TO_DAYS(NOW()) - TO_DAYS(b.datecreated) <= ?';
4051 push @where_args, $newness;
4054 if (@where_strs) {
4055 $query .= 'WHERE ' . join(' AND ', @where_strs);
4058 $query .= q{
4059 GROUP BY b.biblionumber
4060 HAVING count > 0
4061 ORDER BY count DESC
4064 $count = int($count);
4065 if ($count > 0) {
4066 $query .= "LIMIT $count";
4069 my $rows = $dbh->selectall_arrayref($query, { Slice => {} }, @where_args);
4071 return @$rows;
4074 sub _CalculateAndUpdateFine {
4075 my ($params) = @_;
4077 my $borrower = $params->{borrower};
4078 my $item = $params->{item};
4079 my $issue = $params->{issue};
4080 my $return_date = $params->{return_date};
4082 unless ($borrower) { carp "No borrower passed in!" && return; }
4083 unless ($item) { carp "No item passed in!" && return; }
4084 unless ($issue) { carp "No issue passed in!" && return; }
4086 my $datedue = $issue->{date_due};
4088 # we only need to calculate and change the fines if we want to do that on return
4089 # Should be on for hourly loans
4090 my $control = C4::Context->preference('CircControl');
4091 my $control_branchcode =
4092 ( $control eq 'ItemHomeLibrary' ) ? $item->{homebranch}
4093 : ( $control eq 'PatronLibrary' ) ? $borrower->{branchcode}
4094 : $issue->{branchcode};
4096 my $date_returned = $return_date ? dt_from_string($return_date) : dt_from_string();
4098 my ( $amount, $type, $unitcounttotal ) =
4099 C4::Overdues::CalcFine( $item, $borrower->{categorycode}, $control_branchcode, $datedue, $date_returned );
4101 $type ||= q{};
4103 if ( C4::Context->preference('finesMode') eq 'production' ) {
4104 if ( $amount > 0 ) {
4105 C4::Overdues::UpdateFine({
4106 issue_id => $issue->{issue_id},
4107 itemnumber => $issue->{itemnumber},
4108 borrowernumber => $issue->{borrowernumber},
4109 amount => $amount,
4110 type => $type,
4111 due => output_pref($datedue),
4114 elsif ($return_date) {
4116 # Backdated returns may have fines that shouldn't exist,
4117 # so in this case, we need to drop those fines to 0
4119 C4::Overdues::UpdateFine({
4120 issue_id => $issue->{issue_id},
4121 itemnumber => $issue->{itemnumber},
4122 borrowernumber => $issue->{borrowernumber},
4123 amount => 0,
4124 type => $type,
4125 due => output_pref($datedue),
4133 __END__
4135 =head1 AUTHOR
4137 Koha Development Team <http://koha-community.org/>
4139 =cut