Bug 17367 - Showing all items must keep show holdings tab in OPAC details
[koha.git] / C4 / Circulation.pm
blob0cac5edfb7b0502b4fa11941cfa796bb02c9371f
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::Koha qw(
38 GetAuthorisedValueByCode
39 GetAuthValCode
41 use C4::Overdues qw(CalcFine UpdateFine get_chargeable_units);
42 use C4::RotatingCollections qw(GetCollectionItemBranches);
43 use Algorithm::CheckDigits;
45 use Data::Dumper;
46 use Koha::Account;
47 use Koha::DateUtils;
48 use Koha::Calendar;
49 use Koha::Items;
50 use Koha::Patrons;
51 use Koha::Patron::Debarments;
52 use Koha::Database;
53 use Koha::Libraries;
54 use Koha::Holds;
55 use Koha::RefundLostItemFeeRule;
56 use Koha::RefundLostItemFeeRules;
57 use Carp;
58 use List::MoreUtils qw( uniq );
59 use Scalar::Util qw( looks_like_number );
60 use Date::Calc qw(
61 Today
62 Today_and_Now
63 Add_Delta_YM
64 Add_Delta_DHMS
65 Date_to_Days
66 Day_of_Week
67 Add_Delta_Days
69 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
71 BEGIN {
72 require Exporter;
73 @ISA = qw(Exporter);
75 # FIXME subs that should probably be elsewhere
76 push @EXPORT, qw(
77 &barcodedecode
78 &LostItem
79 &ReturnLostItem
80 &GetPendingOnSiteCheckouts
83 # subs to deal with issuing a book
84 push @EXPORT, qw(
85 &CanBookBeIssued
86 &CanBookBeRenewed
87 &AddIssue
88 &AddRenewal
89 &GetRenewCount
90 &GetSoonestRenewDate
91 &GetItemIssue
92 &GetItemIssues
93 &GetIssuingCharges
94 &GetIssuingRule
95 &GetBranchBorrowerCircRule
96 &GetBranchItemRule
97 &GetBiblioIssues
98 &GetOpenIssue
99 &AnonymiseIssueHistory
100 &CheckIfIssuedToPatron
101 &IsItemIssued
102 GetTopIssues
105 # subs to deal with returns
106 push @EXPORT, qw(
107 &AddReturn
108 &MarkIssueReturned
111 # subs to deal with transfers
112 push @EXPORT, qw(
113 &transferbook
114 &GetTransfers
115 &GetTransfersFromTo
116 &updateWrongTransfer
117 &DeleteTransfer
118 &IsBranchTransferAllowed
119 &CreateBranchTransferLimit
120 &DeleteBranchTransferLimits
121 &TransferSlip
124 # subs to deal with offline circulation
125 push @EXPORT, qw(
126 &GetOfflineOperations
127 &GetOfflineOperation
128 &AddOfflineOperation
129 &DeleteOfflineOperation
130 &ProcessOfflineOperation
134 =head1 NAME
136 C4::Circulation - Koha circulation module
138 =head1 SYNOPSIS
140 use C4::Circulation;
142 =head1 DESCRIPTION
144 The functions in this module deal with circulation, issues, and
145 returns, as well as general information about the library.
146 Also deals with inventory.
148 =head1 FUNCTIONS
150 =head2 barcodedecode
152 $str = &barcodedecode($barcode, [$filter]);
154 Generic filter function for barcode string.
155 Called on every circ if the System Pref itemBarcodeInputFilter is set.
156 Will do some manipulation of the barcode for systems that deliver a barcode
157 to circulation.pl that differs from the barcode stored for the item.
158 For proper functioning of this filter, calling the function on the
159 correct barcode string (items.barcode) should return an unaltered barcode.
161 The optional $filter argument is to allow for testing or explicit
162 behavior that ignores the System Pref. Valid values are the same as the
163 System Pref options.
165 =cut
167 # FIXME -- the &decode fcn below should be wrapped into this one.
168 # FIXME -- these plugins should be moved out of Circulation.pm
170 sub barcodedecode {
171 my ($barcode, $filter) = @_;
172 my $branch = C4::Context::mybranch();
173 $filter = C4::Context->preference('itemBarcodeInputFilter') unless $filter;
174 $filter or return $barcode; # ensure filter is defined, else return untouched barcode
175 if ($filter eq 'whitespace') {
176 $barcode =~ s/\s//g;
177 } elsif ($filter eq 'cuecat') {
178 chomp($barcode);
179 my @fields = split( /\./, $barcode );
180 my @results = map( decode($_), @fields[ 1 .. $#fields ] );
181 ($#results == 2) and return $results[2];
182 } elsif ($filter eq 'T-prefix') {
183 if ($barcode =~ /^[Tt](\d)/) {
184 (defined($1) and $1 eq '0') and return $barcode;
185 $barcode = substr($barcode, 2) + 0; # FIXME: probably should be substr($barcode, 1)
187 return sprintf("T%07d", $barcode);
188 # FIXME: $barcode could be "T1", causing warning: substr outside of string
189 # Why drop the nonzero digit after the T?
190 # Why pass non-digits (or empty string) to "T%07d"?
191 } elsif ($filter eq 'libsuite8') {
192 unless($barcode =~ m/^($branch)-/i){ #if barcode starts with branch code its in Koha style. Skip it.
193 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
194 $barcode =~ s/^[0]*(\d+)$/$branch-b-$1/i;
195 }else{
196 $barcode =~ s/^(\D+)[0]*(\d+)$/$branch-$1-$2/i;
199 } elsif ($filter eq 'EAN13') {
200 my $ean = CheckDigits('ean');
201 if ( $ean->is_valid($barcode) ) {
202 #$barcode = sprintf('%013d',$barcode); # this doesn't work on 32-bit systems
203 $barcode = '0' x ( 13 - length($barcode) ) . $barcode;
204 } else {
205 warn "# [$barcode] not valid EAN-13/UPC-A\n";
208 return $barcode; # return barcode, modified or not
211 =head2 decode
213 $str = &decode($chunk);
215 Decodes a segment of a string emitted by a CueCat barcode scanner and
216 returns it.
218 FIXME: Should be replaced with Barcode::Cuecat from CPAN
219 or Javascript based decoding on the client side.
221 =cut
223 sub decode {
224 my ($encoded) = @_;
225 my $seq =
226 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-';
227 my @s = map { index( $seq, $_ ); } split( //, $encoded );
228 my $l = ( $#s + 1 ) % 4;
229 if ($l) {
230 if ( $l == 1 ) {
231 # warn "Error: Cuecat decode parsing failed!";
232 return;
234 $l = 4 - $l;
235 $#s += $l;
237 my $r = '';
238 while ( $#s >= 0 ) {
239 my $n = ( ( $s[0] << 6 | $s[1] ) << 6 | $s[2] ) << 6 | $s[3];
240 $r .=
241 chr( ( $n >> 16 ) ^ 67 )
242 .chr( ( $n >> 8 & 255 ) ^ 67 )
243 .chr( ( $n & 255 ) ^ 67 );
244 @s = @s[ 4 .. $#s ];
246 $r = substr( $r, 0, length($r) - $l );
247 return $r;
250 =head2 transferbook
252 ($dotransfer, $messages, $iteminformation) = &transferbook($newbranch,
253 $barcode, $ignore_reserves);
255 Transfers an item to a new branch. If the item is currently on loan, it is automatically returned before the actual transfer.
257 C<$newbranch> is the code for the branch to which the item should be transferred.
259 C<$barcode> is the barcode of the item to be transferred.
261 If C<$ignore_reserves> is true, C<&transferbook> ignores reserves.
262 Otherwise, if an item is reserved, the transfer fails.
264 Returns three values:
266 =over
268 =item $dotransfer
270 is true if the transfer was successful.
272 =item $messages
274 is a reference-to-hash which may have any of the following keys:
276 =over
278 =item C<BadBarcode>
280 There is no item in the catalog with the given barcode. The value is C<$barcode>.
282 =item C<IsPermanent>
284 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.
286 =item C<DestinationEqualsHolding>
288 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.
290 =item C<WasReturned>
292 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.
294 =item C<ResFound>
296 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>.
298 =item C<WasTransferred>
300 The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored.
302 =back
304 =back
306 =cut
308 sub transferbook {
309 my ( $tbr, $barcode, $ignoreRs ) = @_;
310 my $messages;
311 my $dotransfer = 1;
312 my $itemnumber = GetItemnumberFromBarcode( $barcode );
313 my $issue = GetItemIssue($itemnumber);
314 my $biblio = GetBiblioFromItemNumber($itemnumber);
316 # bad barcode..
317 if ( not $itemnumber ) {
318 $messages->{'BadBarcode'} = $barcode;
319 $dotransfer = 0;
322 # get branches of book...
323 my $hbr = $biblio->{'homebranch'};
324 my $fbr = $biblio->{'holdingbranch'};
326 # if using Branch Transfer Limits
327 if ( C4::Context->preference("UseBranchTransferLimits") == 1 ) {
328 if ( C4::Context->preference("item-level_itypes") && C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ) {
329 if ( ! IsBranchTransferAllowed( $tbr, $fbr, $biblio->{'itype'} ) ) {
330 $messages->{'NotAllowed'} = $tbr . "::" . $biblio->{'itype'};
331 $dotransfer = 0;
333 } elsif ( ! IsBranchTransferAllowed( $tbr, $fbr, $biblio->{ C4::Context->preference("BranchTransferLimitsType") } ) ) {
334 $messages->{'NotAllowed'} = $tbr . "::" . $biblio->{ C4::Context->preference("BranchTransferLimitsType") };
335 $dotransfer = 0;
339 # if is permanent...
340 # FIXME Is this still used by someone?
341 # See other FIXME in AddReturn
342 my $library = Koha::Libraries->find($hbr);
343 if ( $library and $library->get_categories->search({'me.categorycode' => 'PE'})->count ) {
344 $messages->{'IsPermanent'} = $hbr;
345 $dotransfer = 0;
348 # can't transfer book if is already there....
349 if ( $fbr eq $tbr ) {
350 $messages->{'DestinationEqualsHolding'} = 1;
351 $dotransfer = 0;
354 # check if it is still issued to someone, return it...
355 if ($issue->{borrowernumber}) {
356 AddReturn( $barcode, $fbr );
357 $messages->{'WasReturned'} = $issue->{borrowernumber};
360 # find reserves.....
361 # That'll save a database query.
362 my ( $resfound, $resrec, undef ) =
363 CheckReserves( $itemnumber );
364 if ( $resfound and not $ignoreRs ) {
365 $resrec->{'ResFound'} = $resfound;
367 # $messages->{'ResFound'} = $resrec;
368 $dotransfer = 1;
371 #actually do the transfer....
372 if ($dotransfer) {
373 ModItemTransfer( $itemnumber, $fbr, $tbr );
375 # don't need to update MARC anymore, we do it in batch now
376 $messages->{'WasTransfered'} = 1;
379 ModDateLastSeen( $itemnumber );
380 return ( $dotransfer, $messages, $biblio );
384 sub TooMany {
385 my $borrower = shift;
386 my $biblionumber = shift;
387 my $item = shift;
388 my $params = shift;
389 my $onsite_checkout = $params->{onsite_checkout} || 0;
390 my $switch_onsite_checkout = $params->{switch_onsite_checkout} || 0;
391 my $cat_borrower = $borrower->{'categorycode'};
392 my $dbh = C4::Context->dbh;
393 my $branch;
394 # Get which branchcode we need
395 $branch = _GetCircControlBranch($item,$borrower);
396 my $type = (C4::Context->preference('item-level_itypes'))
397 ? $item->{'itype'} # item-level
398 : $item->{'itemtype'}; # biblio-level
400 # given branch, patron category, and item type, determine
401 # applicable issuing rule
402 my $issuing_rule = GetIssuingRule($cat_borrower, $type, $branch);
404 # if a rule is found and has a loan limit set, count
405 # how many loans the patron already has that meet that
406 # rule
407 if (defined($issuing_rule) and defined($issuing_rule->{'maxissueqty'})) {
408 my @bind_params;
409 my $count_query = q|
410 SELECT COUNT(*) AS total, COALESCE(SUM(onsite_checkout), 0) AS onsite_checkouts
411 FROM issues
412 JOIN items USING (itemnumber)
415 my $rule_itemtype = $issuing_rule->{itemtype};
416 if ($rule_itemtype eq "*") {
417 # matching rule has the default item type, so count only
418 # those existing loans that don't fall under a more
419 # specific rule
420 if (C4::Context->preference('item-level_itypes')) {
421 $count_query .= " WHERE items.itype NOT IN (
422 SELECT itemtype FROM issuingrules
423 WHERE branchcode = ?
424 AND (categorycode = ? OR categorycode = ?)
425 AND itemtype <> '*'
426 ) ";
427 } else {
428 $count_query .= " JOIN biblioitems USING (biblionumber)
429 WHERE biblioitems.itemtype NOT IN (
430 SELECT itemtype FROM issuingrules
431 WHERE branchcode = ?
432 AND (categorycode = ? OR categorycode = ?)
433 AND itemtype <> '*'
434 ) ";
436 push @bind_params, $issuing_rule->{branchcode};
437 push @bind_params, $issuing_rule->{categorycode};
438 push @bind_params, $cat_borrower;
439 } else {
440 # rule has specific item type, so count loans of that
441 # specific item type
442 if (C4::Context->preference('item-level_itypes')) {
443 $count_query .= " WHERE items.itype = ? ";
444 } else {
445 $count_query .= " JOIN biblioitems USING (biblionumber)
446 WHERE biblioitems.itemtype= ? ";
448 push @bind_params, $type;
451 $count_query .= " AND borrowernumber = ? ";
452 push @bind_params, $borrower->{'borrowernumber'};
453 my $rule_branch = $issuing_rule->{branchcode};
454 if ($rule_branch ne "*") {
455 if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
456 $count_query .= " AND issues.branchcode = ? ";
457 push @bind_params, $branch;
458 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
459 ; # if branch is the patron's home branch, then count all loans by patron
460 } else {
461 $count_query .= " AND items.homebranch = ? ";
462 push @bind_params, $branch;
466 my ( $checkout_count, $onsite_checkout_count ) = $dbh->selectrow_array( $count_query, {}, @bind_params );
468 my $max_checkouts_allowed = $issuing_rule->{maxissueqty};
469 my $max_onsite_checkouts_allowed = $issuing_rule->{maxonsiteissueqty};
471 if ( $onsite_checkout ) {
472 if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed ) {
473 return {
474 reason => 'TOO_MANY_ONSITE_CHECKOUTS',
475 count => $onsite_checkout_count,
476 max_allowed => $max_onsite_checkouts_allowed,
480 if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) {
481 my $delta = $switch_onsite_checkout ? 1 : 0;
482 if ( $checkout_count >= $max_checkouts_allowed + $delta ) {
483 return {
484 reason => 'TOO_MANY_CHECKOUTS',
485 count => $checkout_count,
486 max_allowed => $max_checkouts_allowed,
489 } elsif ( not $onsite_checkout ) {
490 if ( $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed ) {
491 return {
492 reason => 'TOO_MANY_CHECKOUTS',
493 count => $checkout_count - $onsite_checkout_count,
494 max_allowed => $max_checkouts_allowed,
500 # Now count total loans against the limit for the branch
501 my $branch_borrower_circ_rule = GetBranchBorrowerCircRule($branch, $cat_borrower);
502 if (defined($branch_borrower_circ_rule->{maxissueqty})) {
503 my @bind_params = ();
504 my $branch_count_query = q|
505 SELECT COUNT(*) AS total, COALESCE(SUM(onsite_checkout), 0) AS onsite_checkouts
506 FROM issues
507 JOIN items USING (itemnumber)
508 WHERE borrowernumber = ?
510 push @bind_params, $borrower->{borrowernumber};
512 if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
513 $branch_count_query .= " AND issues.branchcode = ? ";
514 push @bind_params, $branch;
515 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
516 ; # if branch is the patron's home branch, then count all loans by patron
517 } else {
518 $branch_count_query .= " AND items.homebranch = ? ";
519 push @bind_params, $branch;
521 my ( $checkout_count, $onsite_checkout_count ) = $dbh->selectrow_array( $branch_count_query, {}, @bind_params );
522 my $max_checkouts_allowed = $branch_borrower_circ_rule->{maxissueqty};
523 my $max_onsite_checkouts_allowed = $branch_borrower_circ_rule->{maxonsiteissueqty};
525 if ( $onsite_checkout ) {
526 if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed ) {
527 return {
528 reason => 'TOO_MANY_ONSITE_CHECKOUTS',
529 count => $onsite_checkout_count,
530 max_allowed => $max_onsite_checkouts_allowed,
534 if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) {
535 my $delta = $switch_onsite_checkout ? 1 : 0;
536 if ( $checkout_count >= $max_checkouts_allowed + $delta ) {
537 return {
538 reason => 'TOO_MANY_CHECKOUTS',
539 count => $checkout_count,
540 max_allowed => $max_checkouts_allowed,
543 } elsif ( not $onsite_checkout ) {
544 if ( $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed ) {
545 return {
546 reason => 'TOO_MANY_CHECKOUTS',
547 count => $checkout_count - $onsite_checkout_count,
548 max_allowed => $max_checkouts_allowed,
554 # OK, the patron can issue !!!
555 return;
558 =head2 CanBookBeIssued
560 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $borrower,
561 $barcode, $duedate, $inprocess, $ignore_reserves, $params );
563 Check if a book can be issued.
565 C<$issuingimpossible> and C<$needsconfirmation> are some hashref.
567 =over 4
569 =item C<$borrower> hash with borrower informations (from GetMember or GetMemberDetails)
571 =item C<$barcode> is the bar code of the book being issued.
573 =item C<$duedates> is a DateTime object.
575 =item C<$inprocess> boolean switch
577 =item C<$ignore_reserves> boolean switch
579 =item C<$params> Hashref of additional parameters
581 Available keys:
582 override_high_holds - Ignore high holds
583 onsite_checkout - Checkout is an onsite checkout that will not leave the library
585 =back
587 Returns :
589 =over 4
591 =item C<$issuingimpossible> a reference to a hash. It contains reasons why issuing is impossible.
592 Possible values are :
594 =back
596 =head3 INVALID_DATE
598 sticky due date is invalid
600 =head3 GNA
602 borrower gone with no address
604 =head3 CARD_LOST
606 borrower declared it's card lost
608 =head3 DEBARRED
610 borrower debarred
612 =head3 UNKNOWN_BARCODE
614 barcode unknown
616 =head3 NOT_FOR_LOAN
618 item is not for loan
620 =head3 WTHDRAWN
622 item withdrawn.
624 =head3 RESTRICTED
626 item is restricted (set by ??)
628 C<$needsconfirmation> a reference to a hash. It contains reasons why the loan
629 could be prevented, but ones that can be overriden by the operator.
631 Possible values are :
633 =head3 DEBT
635 borrower has debts.
637 =head3 RENEW_ISSUE
639 renewing, not issuing
641 =head3 ISSUED_TO_ANOTHER
643 issued to someone else.
645 =head3 RESERVED
647 reserved for someone else.
649 =head3 INVALID_DATE
651 sticky due date is invalid or due date in the past
653 =head3 TOO_MANY
655 if the borrower borrows to much things
657 =cut
659 sub CanBookBeIssued {
660 my ( $borrower, $barcode, $duedate, $inprocess, $ignore_reserves, $params ) = @_;
661 my %needsconfirmation; # filled with problems that needs confirmations
662 my %issuingimpossible; # filled with problems that causes the issue to be IMPOSSIBLE
663 my %alerts; # filled with messages that shouldn't stop issuing, but the librarian should be aware of.
664 my %messages; # filled with information messages that should be displayed.
666 my $onsite_checkout = $params->{onsite_checkout} || 0;
667 my $override_high_holds = $params->{override_high_holds} || 0;
669 my $item = GetItem(GetItemnumberFromBarcode( $barcode ));
670 my $issue = GetItemIssue($item->{itemnumber});
671 my $biblioitem = GetBiblioItemData($item->{biblioitemnumber});
672 $item->{'itemtype'}=$item->{'itype'};
673 my $dbh = C4::Context->dbh;
675 # MANDATORY CHECKS - unless item exists, nothing else matters
676 unless ( $item->{barcode} ) {
677 $issuingimpossible{UNKNOWN_BARCODE} = 1;
679 return ( \%issuingimpossible, \%needsconfirmation ) if %issuingimpossible;
682 # DUE DATE is OK ? -- should already have checked.
684 if ($duedate && ref $duedate ne 'DateTime') {
685 $duedate = dt_from_string($duedate);
687 my $now = DateTime->now( time_zone => C4::Context->tz() );
688 unless ( $duedate ) {
689 my $issuedate = $now->clone();
691 my $branch = _GetCircControlBranch($item,$borrower);
692 my $itype = ( C4::Context->preference('item-level_itypes') ) ? $item->{'itype'} : $biblioitem->{'itemtype'};
693 $duedate = CalcDateDue( $issuedate, $itype, $branch, $borrower );
695 # Offline circ calls AddIssue directly, doesn't run through here
696 # So issuingimpossible should be ok.
698 if ($duedate) {
699 my $today = $now->clone();
700 $today->truncate( to => 'minute');
701 if (DateTime->compare($duedate,$today) == -1 ) { # duedate cannot be before now
702 $needsconfirmation{INVALID_DATE} = output_pref($duedate);
704 } else {
705 $issuingimpossible{INVALID_DATE} = output_pref($duedate);
709 # BORROWER STATUS
711 if ( $borrower->{'category_type'} eq 'X' && ( $item->{barcode} )) {
712 # stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1 .
713 &UpdateStats({
714 branch => C4::Context->userenv->{'branch'},
715 type => 'localuse',
716 itemnumber => $item->{'itemnumber'},
717 itemtype => $item->{'itemtype'},
718 borrowernumber => $borrower->{'borrowernumber'},
719 ccode => $item->{'ccode'}}
721 ModDateLastSeen( $item->{'itemnumber'} );
722 return( { STATS => 1 }, {});
724 if ( ref $borrower->{flags} ) {
725 if ( $borrower->{flags}->{GNA} ) {
726 $issuingimpossible{GNA} = 1;
728 if ( $borrower->{flags}->{'LOST'} ) {
729 $issuingimpossible{CARD_LOST} = 1;
731 if ( $borrower->{flags}->{'DBARRED'} ) {
732 $issuingimpossible{DEBARRED} = 1;
735 if ( !defined $borrower->{dateexpiry} || $borrower->{'dateexpiry'} eq '0000-00-00') {
736 $issuingimpossible{EXPIRED} = 1;
737 } else {
738 my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'sql', 'floating' );
739 $expiry_dt->truncate( to => 'day');
740 my $today = $now->clone()->truncate(to => 'day');
741 $today->set_time_zone( 'floating' );
742 if ( DateTime->compare($today, $expiry_dt) == 1 ) {
743 $issuingimpossible{EXPIRED} = 1;
748 # BORROWER STATUS
751 # DEBTS
752 my ($balance, $non_issue_charges, $other_charges) =
753 C4::Members::GetMemberAccountBalance( $borrower->{'borrowernumber'} );
755 my $amountlimit = C4::Context->preference("noissuescharge");
756 my $allowfineoverride = C4::Context->preference("AllowFineOverride");
757 my $allfinesneedoverride = C4::Context->preference("AllFinesNeedOverride");
759 # Check the debt of this patrons guarantees
760 my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
761 $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
762 if ( defined $no_issues_charge_guarantees ) {
763 my $p = Koha::Patrons->find( $borrower->{borrowernumber} );
764 my @guarantees = $p->guarantees();
765 my $guarantees_non_issues_charges;
766 foreach my $g ( @guarantees ) {
767 my ( $b, $n, $o ) = C4::Members::GetMemberAccountBalance( $g->id );
768 $guarantees_non_issues_charges += $n;
771 if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && !$allowfineoverride) {
772 $issuingimpossible{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
773 } elsif ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && $allowfineoverride) {
774 $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
775 } elsif ( $allfinesneedoverride && $guarantees_non_issues_charges > 0 && $guarantees_non_issues_charges <= $no_issues_charge_guarantees && !$inprocess ) {
776 $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
780 if ( C4::Context->preference("IssuingInProcess") ) {
781 if ( $non_issue_charges > $amountlimit && !$inprocess && !$allowfineoverride) {
782 $issuingimpossible{DEBT} = sprintf( "%.2f", $non_issue_charges );
783 } elsif ( $non_issue_charges > $amountlimit && !$inprocess && $allowfineoverride) {
784 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
785 } elsif ( $allfinesneedoverride && $non_issue_charges > 0 && $non_issue_charges <= $amountlimit && !$inprocess ) {
786 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
789 else {
790 if ( $non_issue_charges > $amountlimit && $allowfineoverride ) {
791 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
792 } elsif ( $non_issue_charges > $amountlimit && !$allowfineoverride) {
793 $issuingimpossible{DEBT} = sprintf( "%.2f", $non_issue_charges );
794 } elsif ( $non_issue_charges > 0 && $allfinesneedoverride ) {
795 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
799 if ($balance > 0 && $other_charges > 0) {
800 $alerts{OTHER_CHARGES} = sprintf( "%.2f", $other_charges );
803 my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
804 if ( my $debarred_date = $patron->is_debarred ) {
805 # patron has accrued fine days or has a restriction. $count is a date
806 if ($debarred_date eq '9999-12-31') {
807 $issuingimpossible{USERBLOCKEDNOENDDATE} = $debarred_date;
809 else {
810 $issuingimpossible{USERBLOCKEDWITHENDDATE} = $debarred_date;
812 } elsif ( my $num_overdues = $patron->has_overdues ) {
813 ## patron has outstanding overdue loans
814 if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){
815 $issuingimpossible{USERBLOCKEDOVERDUE} = $num_overdues;
817 elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){
818 $needsconfirmation{USERBLOCKEDOVERDUE} = $num_overdues;
822 # JB34 CHECKS IF BORROWERS DON'T HAVE ISSUE TOO MANY BOOKS
824 my $switch_onsite_checkout =
825 C4::Context->preference('SwitchOnSiteCheckouts')
826 and $issue->{onsite_checkout}
827 and $issue
828 and $issue->{borrowernumber} == $borrower->{'borrowernumber'} ? 1 : 0;
829 my $toomany = TooMany( $borrower, $item->{biblionumber}, $item, { onsite_checkout => $onsite_checkout, switch_onsite_checkout => $switch_onsite_checkout, } );
830 # if TooMany max_allowed returns 0 the user doesn't have permission to check out this book
831 if ( $toomany ) {
832 if ( $toomany->{max_allowed} == 0 ) {
833 $needsconfirmation{PATRON_CANT} = 1;
835 if ( C4::Context->preference("AllowTooManyOverride") ) {
836 $needsconfirmation{TOO_MANY} = $toomany->{reason};
837 $needsconfirmation{current_loan_count} = $toomany->{count};
838 $needsconfirmation{max_loans_allowed} = $toomany->{max_allowed};
839 } else {
840 $issuingimpossible{TOO_MANY} = $toomany->{reason};
841 $issuingimpossible{current_loan_count} = $toomany->{count};
842 $issuingimpossible{max_loans_allowed} = $toomany->{max_allowed};
847 # CHECKPREVCHECKOUT: CHECK IF ITEM HAS EVER BEEN LENT TO PATRON
849 $patron = Koha::Patrons->find($borrower->{borrowernumber});
850 my $wants_check = $patron->wants_check_for_previous_checkout;
851 $needsconfirmation{PREVISSUE} = 1
852 if ($wants_check and $patron->do_check_for_previous_checkout($item));
855 # ITEM CHECKING
857 if ( $item->{'notforloan'} )
859 if(!C4::Context->preference("AllowNotForLoanOverride")){
860 $issuingimpossible{NOT_FOR_LOAN} = 1;
861 $issuingimpossible{item_notforloan} = $item->{'notforloan'};
862 }else{
863 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
864 $needsconfirmation{item_notforloan} = $item->{'notforloan'};
867 else {
868 # we have to check itemtypes.notforloan also
869 if (C4::Context->preference('item-level_itypes')){
870 # this should probably be a subroutine
871 my $sth = $dbh->prepare("SELECT notforloan FROM itemtypes WHERE itemtype = ?");
872 $sth->execute($item->{'itemtype'});
873 my $notforloan=$sth->fetchrow_hashref();
874 if ($notforloan->{'notforloan'}) {
875 if (!C4::Context->preference("AllowNotForLoanOverride")) {
876 $issuingimpossible{NOT_FOR_LOAN} = 1;
877 $issuingimpossible{itemtype_notforloan} = $item->{'itype'};
878 } else {
879 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
880 $needsconfirmation{itemtype_notforloan} = $item->{'itype'};
884 elsif ($biblioitem->{'notforloan'} == 1){
885 if (!C4::Context->preference("AllowNotForLoanOverride")) {
886 $issuingimpossible{NOT_FOR_LOAN} = 1;
887 $issuingimpossible{itemtype_notforloan} = $biblioitem->{'itemtype'};
888 } else {
889 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
890 $needsconfirmation{itemtype_notforloan} = $biblioitem->{'itemtype'};
894 if ( $item->{'withdrawn'} && $item->{'withdrawn'} > 0 )
896 $issuingimpossible{WTHDRAWN} = 1;
898 if ( $item->{'restricted'}
899 && $item->{'restricted'} == 1 )
901 $issuingimpossible{RESTRICTED} = 1;
903 if ( $item->{'itemlost'} && C4::Context->preference("IssueLostItem") ne 'nothing' ) {
904 my $code = GetAuthorisedValueByCode( 'LOST', $item->{'itemlost'} );
905 $needsconfirmation{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'confirm' );
906 $alerts{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'alert' );
908 if ( C4::Context->preference("IndependentBranches") ) {
909 my $userenv = C4::Context->userenv;
910 unless ( C4::Context->IsSuperLibrarian() ) {
911 if ( $item->{C4::Context->preference("HomeOrHoldingBranch")} ne $userenv->{branch} ){
912 $issuingimpossible{ITEMNOTSAMEBRANCH} = 1;
913 $issuingimpossible{'itemhomebranch'} = $item->{C4::Context->preference("HomeOrHoldingBranch")};
915 $needsconfirmation{BORRNOTSAMEBRANCH} = $borrower->{'branchcode'}
916 if ( $borrower->{'branchcode'} ne $userenv->{branch} );
920 # CHECK IF THERE IS RENTAL CHARGES. RENTAL MUST BE CONFIRMED BY THE BORROWER
922 my $rentalConfirmation = C4::Context->preference("RentalFeesCheckoutConfirmation");
924 if ( $rentalConfirmation ){
925 my ($rentalCharge) = GetIssuingCharges( $item->{'itemnumber'}, $borrower->{'borrowernumber'} );
926 if ( $rentalCharge > 0 ){
927 $rentalCharge = sprintf("%.02f", $rentalCharge);
928 $needsconfirmation{RENTALCHARGE} = $rentalCharge;
933 # CHECK IF BOOK ALREADY ISSUED TO THIS BORROWER
935 if ( $issue->{borrowernumber} && $issue->{borrowernumber} eq $borrower->{'borrowernumber'} ){
937 # Already issued to current borrower.
938 # If it is an on-site checkout if it can be switched to a normal checkout
939 # or ask whether the loan should be renewed
941 if ( $issue->{onsite_checkout}
942 and C4::Context->preference('SwitchOnSiteCheckouts') ) {
943 $messages{ONSITE_CHECKOUT_WILL_BE_SWITCHED} = 1;
944 } else {
945 my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed(
946 $borrower->{'borrowernumber'},
947 $item->{'itemnumber'},
949 if ( $CanBookBeRenewed == 0 ) { # no more renewals allowed
950 if ( $renewerror eq 'onsite_checkout' ) {
951 $issuingimpossible{NO_RENEWAL_FOR_ONSITE_CHECKOUTS} = 1;
953 else {
954 $issuingimpossible{NO_MORE_RENEWALS} = 1;
957 else {
958 $needsconfirmation{RENEW_ISSUE} = 1;
962 elsif ($issue->{borrowernumber}) {
964 # issued to someone else
965 my $currborinfo = C4::Members::GetMember( borrowernumber => $issue->{borrowernumber} );
968 my ( $can_be_returned, $message ) = CanBookBeReturned( $item, C4::Context->userenv->{branch} );
970 unless ( $can_be_returned ) {
971 $issuingimpossible{RETURN_IMPOSSIBLE} = 1;
972 $issuingimpossible{branch_to_return} = $message;
973 } else {
974 $needsconfirmation{ISSUED_TO_ANOTHER} = 1;
975 $needsconfirmation{issued_firstname} = $currborinfo->{'firstname'};
976 $needsconfirmation{issued_surname} = $currborinfo->{'surname'};
977 $needsconfirmation{issued_cardnumber} = $currborinfo->{'cardnumber'};
978 $needsconfirmation{issued_borrowernumber} = $currborinfo->{'borrowernumber'};
982 unless ( $ignore_reserves ) {
983 # See if the item is on reserve.
984 my ( $restype, $res ) = C4::Reserves::CheckReserves( $item->{'itemnumber'} );
985 if ($restype) {
986 my $resbor = $res->{'borrowernumber'};
987 if ( $resbor ne $borrower->{'borrowernumber'} ) {
988 my ( $resborrower ) = C4::Members::GetMember( borrowernumber => $resbor );
989 if ( $restype eq "Waiting" )
991 # The item is on reserve and waiting, but has been
992 # reserved by some other patron.
993 $needsconfirmation{RESERVE_WAITING} = 1;
994 $needsconfirmation{'resfirstname'} = $resborrower->{'firstname'};
995 $needsconfirmation{'ressurname'} = $resborrower->{'surname'};
996 $needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'};
997 $needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'};
998 $needsconfirmation{'resbranchcode'} = $res->{branchcode};
999 $needsconfirmation{'reswaitingdate'} = $res->{'waitingdate'};
1001 elsif ( $restype eq "Reserved" ) {
1002 # The item is on reserve for someone else.
1003 $needsconfirmation{RESERVED} = 1;
1004 $needsconfirmation{'resfirstname'} = $resborrower->{'firstname'};
1005 $needsconfirmation{'ressurname'} = $resborrower->{'surname'};
1006 $needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'};
1007 $needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'};
1008 $needsconfirmation{'resbranchcode'} = $res->{branchcode};
1009 $needsconfirmation{'resreservedate'} = $res->{'reservedate'};
1015 ## CHECK AGE RESTRICTION
1016 my $agerestriction = $biblioitem->{'agerestriction'};
1017 my ($restriction_age, $daysToAgeRestriction) = GetAgeRestriction( $agerestriction, $borrower );
1018 if ( $daysToAgeRestriction && $daysToAgeRestriction > 0 ) {
1019 if ( C4::Context->preference('AgeRestrictionOverride') ) {
1020 $needsconfirmation{AGE_RESTRICTION} = "$agerestriction";
1022 else {
1023 $issuingimpossible{AGE_RESTRICTION} = "$agerestriction";
1027 ## check for high holds decreasing loan period
1028 if ( C4::Context->preference('decreaseLoanHighHolds') ) {
1029 my $check = checkHighHolds( $item, $borrower );
1031 if ( $check->{exceeded} ) {
1032 if ($override_high_holds) {
1033 $alerts{HIGHHOLDS} = {
1034 num_holds => $check->{outstanding},
1035 duration => $check->{duration},
1036 returndate => output_pref( $check->{due_date} ),
1039 else {
1040 $needsconfirmation{HIGHHOLDS} = {
1041 num_holds => $check->{outstanding},
1042 duration => $check->{duration},
1043 returndate => output_pref( $check->{due_date} ),
1049 if (
1050 !C4::Context->preference('AllowMultipleIssuesOnABiblio') &&
1051 # don't do the multiple loans per bib check if we've
1052 # already determined that we've got a loan on the same item
1053 !$issuingimpossible{NO_MORE_RENEWALS} &&
1054 !$needsconfirmation{RENEW_ISSUE}
1056 # Check if borrower has already issued an item from the same biblio
1057 # Only if it's not a subscription
1058 my $biblionumber = $item->{biblionumber};
1059 require C4::Serials;
1060 my $is_a_subscription = C4::Serials::CountSubscriptionFromBiblionumber($biblionumber);
1061 unless ($is_a_subscription) {
1062 my $issues = GetIssues( {
1063 borrowernumber => $borrower->{borrowernumber},
1064 biblionumber => $biblionumber,
1065 } );
1066 my @issues = $issues ? @$issues : ();
1067 # if we get here, we don't already have a loan on this item,
1068 # so if there are any loans on this bib, ask for confirmation
1069 if (scalar @issues > 0) {
1070 $needsconfirmation{BIBLIO_ALREADY_ISSUED} = 1;
1075 return ( \%issuingimpossible, \%needsconfirmation, \%alerts, \%messages, );
1078 =head2 CanBookBeReturned
1080 ($returnallowed, $message) = CanBookBeReturned($item, $branch)
1082 Check whether the item can be returned to the provided branch
1084 =over 4
1086 =item C<$item> is a hash of item information as returned from GetItem
1088 =item C<$branch> is the branchcode where the return is taking place
1090 =back
1092 Returns:
1094 =over 4
1096 =item C<$returnallowed> is 0 or 1, corresponding to whether the return is allowed (1) or not (0)
1098 =item C<$message> is the branchcode where the item SHOULD be returned, if the return is not allowed
1100 =back
1102 =cut
1104 sub CanBookBeReturned {
1105 my ($item, $branch) = @_;
1106 my $allowreturntobranch = C4::Context->preference("AllowReturnToBranch") || 'anywhere';
1108 # assume return is allowed to start
1109 my $allowed = 1;
1110 my $message;
1112 # identify all cases where return is forbidden
1113 if ($allowreturntobranch eq 'homebranch' && $branch ne $item->{'homebranch'}) {
1114 $allowed = 0;
1115 $message = $item->{'homebranch'};
1116 } elsif ($allowreturntobranch eq 'holdingbranch' && $branch ne $item->{'holdingbranch'}) {
1117 $allowed = 0;
1118 $message = $item->{'holdingbranch'};
1119 } elsif ($allowreturntobranch eq 'homeorholdingbranch' && $branch ne $item->{'homebranch'} && $branch ne $item->{'holdingbranch'}) {
1120 $allowed = 0;
1121 $message = $item->{'homebranch'}; # FIXME: choice of homebranch is arbitrary
1124 return ($allowed, $message);
1127 =head2 CheckHighHolds
1129 used when syspref decreaseLoanHighHolds is active. Returns 1 or 0 to define whether the minimum value held in
1130 decreaseLoanHighHoldsValue is exceeded, the total number of outstanding holds, the number of days the loan
1131 has been decreased to (held in syspref decreaseLoanHighHoldsValue), and the new due date
1133 =cut
1135 sub checkHighHolds {
1136 my ( $item, $borrower ) = @_;
1137 my $biblio = GetBiblioFromItemNumber( $item->{itemnumber} );
1138 my $branch = _GetCircControlBranch( $item, $borrower );
1140 my $return_data = {
1141 exceeded => 0,
1142 outstanding => 0,
1143 duration => 0,
1144 due_date => undef,
1147 my $holds = Koha::Holds->search( { biblionumber => $item->{'biblionumber'} } );
1149 if ( $holds->count() ) {
1150 $return_data->{outstanding} = $holds->count();
1152 my $decreaseLoanHighHoldsControl = C4::Context->preference('decreaseLoanHighHoldsControl');
1153 my $decreaseLoanHighHoldsValue = C4::Context->preference('decreaseLoanHighHoldsValue');
1154 my $decreaseLoanHighHoldsIgnoreStatuses = C4::Context->preference('decreaseLoanHighHoldsIgnoreStatuses');
1156 my @decreaseLoanHighHoldsIgnoreStatuses = split( /,/, $decreaseLoanHighHoldsIgnoreStatuses );
1158 if ( $decreaseLoanHighHoldsControl eq 'static' ) {
1160 # static means just more than a given number of holds on the record
1162 # If the number of holds is less than the threshold, we can stop here
1163 if ( $holds->count() < $decreaseLoanHighHoldsValue ) {
1164 return $return_data;
1167 elsif ( $decreaseLoanHighHoldsControl eq 'dynamic' ) {
1169 # dynamic means X more than the number of holdable items on the record
1171 # let's get the items
1172 my @items = $holds->next()->biblio()->items();
1174 # Remove any items with status defined to be ignored even if the would not make item unholdable
1175 foreach my $status (@decreaseLoanHighHoldsIgnoreStatuses) {
1176 @items = grep { !$_->$status } @items;
1179 # Remove any items that are not holdable for this patron
1180 @items = grep { CanItemBeReserved( $borrower->{borrowernumber}, $_->itemnumber ) eq 'OK' } @items;
1182 my $items_count = scalar @items;
1184 my $threshold = $items_count + $decreaseLoanHighHoldsValue;
1186 # If the number of holds is less than the count of items we have
1187 # plus the number of holds allowed above that count, we can stop here
1188 if ( $holds->count() <= $threshold ) {
1189 return $return_data;
1193 my $issuedate = DateTime->now( time_zone => C4::Context->tz() );
1195 my $calendar = Koha::Calendar->new( branchcode => $branch );
1197 my $itype =
1198 ( C4::Context->preference('item-level_itypes') )
1199 ? $biblio->{'itype'}
1200 : $biblio->{'itemtype'};
1202 my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branch, $borrower );
1204 my $decreaseLoanHighHoldsDuration = C4::Context->preference('decreaseLoanHighHoldsDuration');
1206 my $reduced_datedue = $calendar->addDate( $issuedate, $decreaseLoanHighHoldsDuration );
1208 if ( DateTime->compare( $reduced_datedue, $orig_due ) == -1 ) {
1209 $return_data->{exceeded} = 1;
1210 $return_data->{duration} = $decreaseLoanHighHoldsDuration;
1211 $return_data->{due_date} = $reduced_datedue;
1215 return $return_data;
1218 =head2 AddIssue
1220 &AddIssue($borrower, $barcode, [$datedue], [$cancelreserve], [$issuedate])
1222 Issue a book. Does no check, they are done in CanBookBeIssued. If we reach this sub, it means the user confirmed if needed.
1224 =over 4
1226 =item C<$borrower> is a hash with borrower informations (from GetMember or GetMemberDetails).
1228 =item C<$barcode> is the barcode of the item being issued.
1230 =item C<$datedue> is a DateTime object for the max date of return, i.e. the date due (optional).
1231 Calculated if empty.
1233 =item C<$cancelreserve> is 1 to override and cancel any pending reserves for the item (optional).
1235 =item C<$issuedate> is the date to issue the item in iso (YYYY-MM-DD) format (optional).
1236 Defaults to today. Unlike C<$datedue>, NOT a DateTime object, unfortunately.
1238 AddIssue does the following things :
1240 - step 01: check that there is a borrowernumber & a barcode provided
1241 - check for RENEWAL (book issued & being issued to the same patron)
1242 - renewal YES = Calculate Charge & renew
1243 - renewal NO =
1244 * BOOK ACTUALLY ISSUED ? do a return if book is actually issued (but to someone else)
1245 * RESERVE PLACED ?
1246 - fill reserve if reserve to this patron
1247 - cancel reserve or not, otherwise
1248 * TRANSFERT PENDING ?
1249 - complete the transfert
1250 * ISSUE THE BOOK
1252 =back
1254 =cut
1256 sub AddIssue {
1257 my ( $borrower, $barcode, $datedue, $cancelreserve, $issuedate, $sipmode, $params ) = @_;
1259 my $onsite_checkout = $params && $params->{onsite_checkout} ? 1 : 0;
1260 my $switch_onsite_checkout = $params && $params->{switch_onsite_checkout};
1261 my $auto_renew = $params && $params->{auto_renew};
1262 my $dbh = C4::Context->dbh;
1263 my $barcodecheck = CheckValidBarcode($barcode);
1265 my $issue;
1267 if ( $datedue && ref $datedue ne 'DateTime' ) {
1268 $datedue = dt_from_string($datedue);
1271 # $issuedate defaults to today.
1272 if ( !defined $issuedate ) {
1273 $issuedate = DateTime->now( time_zone => C4::Context->tz() );
1275 else {
1276 if ( ref $issuedate ne 'DateTime' ) {
1277 $issuedate = dt_from_string($issuedate);
1282 # Stop here if the patron or barcode doesn't exist
1283 if ( $borrower && $barcode && $barcodecheck ) {
1284 # find which item we issue
1285 my $item = GetItem( '', $barcode )
1286 or return; # if we don't get an Item, abort.
1288 my $branch = _GetCircControlBranch( $item, $borrower );
1290 # get actual issuing if there is one
1291 my $actualissue = GetItemIssue( $item->{itemnumber} );
1293 # get biblioinformation for this item
1294 my $biblio = GetBiblioFromItemNumber( $item->{itemnumber} );
1296 # check if we just renew the issue.
1297 if ( $actualissue->{borrowernumber} eq $borrower->{'borrowernumber'}
1298 and not $switch_onsite_checkout ) {
1299 $datedue = AddRenewal(
1300 $borrower->{'borrowernumber'},
1301 $item->{'itemnumber'},
1302 $branch,
1303 $datedue,
1304 $issuedate, # here interpreted as the renewal date
1307 else {
1308 # it's NOT a renewal
1309 if ( $actualissue->{borrowernumber}
1310 and not $switch_onsite_checkout ) {
1311 # This book is currently on loan, but not to the person
1312 # who wants to borrow it now. mark it returned before issuing to the new borrower
1313 my ( $allowed, $message ) = CanBookBeReturned( $item, C4::Context->userenv->{branch} );
1314 return unless $allowed;
1315 AddReturn( $item->{'barcode'}, C4::Context->userenv->{'branch'} );
1318 MoveReserve( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $cancelreserve );
1320 # Starting process for transfer job (checking transfert and validate it if we have one)
1321 my ($datesent) = GetTransfers( $item->{'itemnumber'} );
1322 if ($datesent) {
1323 # updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....)
1324 my $sth = $dbh->prepare(
1325 "UPDATE branchtransfers
1326 SET datearrived = now(),
1327 tobranch = ?,
1328 comments = 'Forced branchtransfer'
1329 WHERE itemnumber= ? AND datearrived IS NULL"
1331 $sth->execute( C4::Context->userenv->{'branch'},
1332 $item->{'itemnumber'} );
1335 # If automatic renewal wasn't selected while issuing, set the value according to the issuing rule.
1336 unless ($auto_renew) {
1337 my $issuingrule = GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branch );
1338 $auto_renew = $issuingrule->{auto_renew};
1341 # Record in the database the fact that the book was issued.
1342 unless ($datedue) {
1343 my $itype =
1344 ( C4::Context->preference('item-level_itypes') )
1345 ? $biblio->{'itype'}
1346 : $biblio->{'itemtype'};
1347 $datedue = CalcDateDue( $issuedate, $itype, $branch, $borrower );
1350 $datedue->truncate( to => 'minute' );
1352 $issue = Koha::Database->new()->schema()->resultset('Issue')->update_or_create(
1354 borrowernumber => $borrower->{'borrowernumber'},
1355 itemnumber => $item->{'itemnumber'},
1356 issuedate => $issuedate->strftime('%Y-%m-%d %H:%M:%S'),
1357 date_due => $datedue->strftime('%Y-%m-%d %H:%M:%S'),
1358 branchcode => C4::Context->userenv->{'branch'},
1359 onsite_checkout => $onsite_checkout,
1360 auto_renew => $auto_renew ? 1 : 0
1364 if ( C4::Context->preference('ReturnToShelvingCart') ) {
1365 # ReturnToShelvingCart is on, anything issued should be taken off the cart.
1366 CartToShelf( $item->{'itemnumber'} );
1368 $item->{'issues'}++;
1369 if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) {
1370 UpdateTotalIssues( $item->{'biblionumber'}, 1 );
1373 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1374 if ( $item->{'itemlost'} ) {
1375 if (
1376 Koha::RefundLostItemFeeRules->should_refund(
1378 current_branch => C4::Context->userenv->{branch},
1379 item_home_branch => $item->{homebranch},
1380 item_holding_branch => $item->{holdingbranch}
1385 _FixAccountForLostAndReturned( $item->{'itemnumber'}, undef,
1386 $item->{'barcode'} );
1390 ModItem(
1392 issues => $item->{'issues'},
1393 holdingbranch => C4::Context->userenv->{'branch'},
1394 itemlost => 0,
1395 onloan => $datedue->ymd(),
1396 datelastborrowed => DateTime->now( time_zone => C4::Context->tz() )->ymd(),
1398 $item->{'biblionumber'},
1399 $item->{'itemnumber'}
1401 ModDateLastSeen( $item->{'itemnumber'} );
1403 # If it costs to borrow this book, charge it to the patron's account.
1404 my ( $charge, $itemtype ) = GetIssuingCharges( $item->{'itemnumber'}, $borrower->{'borrowernumber'} );
1405 if ( $charge > 0 ) {
1406 AddIssuingCharge( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $charge );
1407 $item->{'charge'} = $charge;
1410 # Record the fact that this book was issued.
1411 &UpdateStats(
1413 branch => C4::Context->userenv->{'branch'},
1414 type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
1415 amount => $charge,
1416 other => ( $sipmode ? "SIP-$sipmode" : '' ),
1417 itemnumber => $item->{'itemnumber'},
1418 itemtype => $item->{'itype'},
1419 borrowernumber => $borrower->{'borrowernumber'},
1420 ccode => $item->{'ccode'}
1424 # Send a checkout slip.
1425 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1426 my %conditions = (
1427 branchcode => $branch,
1428 categorycode => $borrower->{categorycode},
1429 item_type => $item->{itype},
1430 notification => 'CHECKOUT',
1432 if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
1433 SendCirculationAlert(
1435 type => 'CHECKOUT',
1436 item => $item,
1437 borrower => $borrower,
1438 branch => $branch,
1444 logaction(
1445 "CIRCULATION", "ISSUE",
1446 $borrower->{'borrowernumber'},
1447 $biblio->{'itemnumber'}
1448 ) if C4::Context->preference("IssueLog");
1450 return $issue;
1453 =head2 GetLoanLength
1455 my $loanlength = &GetLoanLength($borrowertype,$itemtype,branchcode)
1457 Get loan length for an itemtype, a borrower type and a branch
1459 =cut
1461 sub GetLoanLength {
1462 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1463 my $dbh = C4::Context->dbh;
1464 my $sth = $dbh->prepare(qq{
1465 SELECT issuelength, lengthunit, renewalperiod
1466 FROM issuingrules
1467 WHERE categorycode=?
1468 AND itemtype=?
1469 AND branchcode=?
1470 AND issuelength IS NOT NULL
1473 # try to find issuelength & return the 1st available.
1474 # check with borrowertype, itemtype and branchcode, then without one of those parameters
1475 $sth->execute( $borrowertype, $itemtype, $branchcode );
1476 my $loanlength = $sth->fetchrow_hashref;
1478 return $loanlength
1479 if defined($loanlength) && defined $loanlength->{issuelength};
1481 $sth->execute( $borrowertype, '*', $branchcode );
1482 $loanlength = $sth->fetchrow_hashref;
1483 return $loanlength
1484 if defined($loanlength) && defined $loanlength->{issuelength};
1486 $sth->execute( '*', $itemtype, $branchcode );
1487 $loanlength = $sth->fetchrow_hashref;
1488 return $loanlength
1489 if defined($loanlength) && defined $loanlength->{issuelength};
1491 $sth->execute( '*', '*', $branchcode );
1492 $loanlength = $sth->fetchrow_hashref;
1493 return $loanlength
1494 if defined($loanlength) && defined $loanlength->{issuelength};
1496 $sth->execute( $borrowertype, $itemtype, '*' );
1497 $loanlength = $sth->fetchrow_hashref;
1498 return $loanlength
1499 if defined($loanlength) && defined $loanlength->{issuelength};
1501 $sth->execute( $borrowertype, '*', '*' );
1502 $loanlength = $sth->fetchrow_hashref;
1503 return $loanlength
1504 if defined($loanlength) && defined $loanlength->{issuelength};
1506 $sth->execute( '*', $itemtype, '*' );
1507 $loanlength = $sth->fetchrow_hashref;
1508 return $loanlength
1509 if defined($loanlength) && defined $loanlength->{issuelength};
1511 $sth->execute( '*', '*', '*' );
1512 $loanlength = $sth->fetchrow_hashref;
1513 return $loanlength
1514 if defined($loanlength) && defined $loanlength->{issuelength};
1516 # if no rule is set => 0 day (hardcoded)
1517 return {
1518 issuelength => 0,
1519 renewalperiod => 0,
1520 lengthunit => 'days',
1526 =head2 GetHardDueDate
1528 my ($hardduedate,$hardduedatecompare) = &GetHardDueDate($borrowertype,$itemtype,branchcode)
1530 Get the Hard Due Date and it's comparison for an itemtype, a borrower type and a branch
1532 =cut
1534 sub GetHardDueDate {
1535 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1537 my $rule = GetIssuingRule( $borrowertype, $itemtype, $branchcode );
1539 if ( defined( $rule ) ) {
1540 if ( $rule->{hardduedate} ) {
1541 return (dt_from_string($rule->{hardduedate}, 'iso'),$rule->{hardduedatecompare});
1542 } else {
1543 return (undef, undef);
1548 =head2 GetIssuingRule
1550 my $irule = &GetIssuingRule($borrowertype,$itemtype,branchcode)
1552 FIXME - This is a copy-paste of GetLoanLength
1553 as a stop-gap. Do not wish to change API for GetLoanLength
1554 this close to release.
1556 Get the issuing rule for an itemtype, a borrower type and a branch
1557 Returns a hashref from the issuingrules table.
1559 =cut
1561 sub GetIssuingRule {
1562 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1563 my $dbh = C4::Context->dbh;
1564 my $sth = $dbh->prepare( "select * from issuingrules where categorycode=? and itemtype=? and branchcode=?" );
1565 my $irule;
1567 $sth->execute( $borrowertype, $itemtype, $branchcode );
1568 $irule = $sth->fetchrow_hashref;
1569 return $irule if defined($irule) ;
1571 $sth->execute( $borrowertype, "*", $branchcode );
1572 $irule = $sth->fetchrow_hashref;
1573 return $irule if defined($irule) ;
1575 $sth->execute( "*", $itemtype, $branchcode );
1576 $irule = $sth->fetchrow_hashref;
1577 return $irule if defined($irule) ;
1579 $sth->execute( "*", "*", $branchcode );
1580 $irule = $sth->fetchrow_hashref;
1581 return $irule if defined($irule) ;
1583 $sth->execute( $borrowertype, $itemtype, "*" );
1584 $irule = $sth->fetchrow_hashref;
1585 return $irule if defined($irule) ;
1587 $sth->execute( $borrowertype, "*", "*" );
1588 $irule = $sth->fetchrow_hashref;
1589 return $irule if defined($irule) ;
1591 $sth->execute( "*", $itemtype, "*" );
1592 $irule = $sth->fetchrow_hashref;
1593 return $irule if defined($irule) ;
1595 $sth->execute( "*", "*", "*" );
1596 $irule = $sth->fetchrow_hashref;
1597 return $irule if defined($irule) ;
1599 # if no rule matches,
1600 return;
1603 =head2 GetBranchBorrowerCircRule
1605 my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
1607 Retrieves circulation rule attributes that apply to the given
1608 branch and patron category, regardless of item type.
1609 The return value is a hashref containing the following key:
1611 maxissueqty - maximum number of loans that a
1612 patron of the given category can have at the given
1613 branch. If the value is undef, no limit.
1615 maxonsiteissueqty - maximum of on-site checkouts that a
1616 patron of the given category can have at the given
1617 branch. If the value is undef, no limit.
1619 This will first check for a specific branch and
1620 category match from branch_borrower_circ_rules.
1622 If no rule is found, it will then check default_branch_circ_rules
1623 (same branch, default category). If no rule is found,
1624 it will then check default_borrower_circ_rules (default
1625 branch, same category), then failing that, default_circ_rules
1626 (default branch, default category).
1628 If no rule has been found in the database, it will default to
1629 the buillt in rule:
1631 maxissueqty - undef
1632 maxonsiteissueqty - undef
1634 C<$branchcode> and C<$categorycode> should contain the
1635 literal branch code and patron category code, respectively - no
1636 wildcards.
1638 =cut
1640 sub GetBranchBorrowerCircRule {
1641 my ( $branchcode, $categorycode ) = @_;
1643 my $rules;
1644 my $dbh = C4::Context->dbh();
1645 $rules = $dbh->selectrow_hashref( q|
1646 SELECT maxissueqty, maxonsiteissueqty
1647 FROM branch_borrower_circ_rules
1648 WHERE branchcode = ?
1649 AND categorycode = ?
1650 |, {}, $branchcode, $categorycode ) ;
1651 return $rules if $rules;
1653 # try same branch, default borrower category
1654 $rules = $dbh->selectrow_hashref( q|
1655 SELECT maxissueqty, maxonsiteissueqty
1656 FROM default_branch_circ_rules
1657 WHERE branchcode = ?
1658 |, {}, $branchcode ) ;
1659 return $rules if $rules;
1661 # try default branch, same borrower category
1662 $rules = $dbh->selectrow_hashref( q|
1663 SELECT maxissueqty, maxonsiteissueqty
1664 FROM default_borrower_circ_rules
1665 WHERE categorycode = ?
1666 |, {}, $categorycode ) ;
1667 return $rules if $rules;
1669 # try default branch, default borrower category
1670 $rules = $dbh->selectrow_hashref( q|
1671 SELECT maxissueqty, maxonsiteissueqty
1672 FROM default_circ_rules
1673 |, {} );
1674 return $rules if $rules;
1676 # built-in default circulation rule
1677 return {
1678 maxissueqty => undef,
1679 maxonsiteissueqty => undef,
1683 =head2 GetBranchItemRule
1685 my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
1687 Retrieves circulation rule attributes that apply to the given
1688 branch and item type, regardless of patron category.
1690 The return value is a hashref containing the following keys:
1692 holdallowed => Hold policy for this branch and itemtype. Possible values:
1693 0: No holds allowed.
1694 1: Holds allowed only by patrons that have the same homebranch as the item.
1695 2: Holds allowed from any patron.
1697 returnbranch => branch to which to return item. Possible values:
1698 noreturn: do not return, let item remain where checked in (floating collections)
1699 homebranch: return to item's home branch
1700 holdingbranch: return to issuer branch
1702 This searches branchitemrules in the following order:
1704 * Same branchcode and itemtype
1705 * Same branchcode, itemtype '*'
1706 * branchcode '*', same itemtype
1707 * branchcode and itemtype '*'
1709 Neither C<$branchcode> nor C<$itemtype> should be '*'.
1711 =cut
1713 sub GetBranchItemRule {
1714 my ( $branchcode, $itemtype ) = @_;
1715 my $dbh = C4::Context->dbh();
1716 my $result = {};
1718 my @attempts = (
1719 ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1720 FROM branch_item_rules
1721 WHERE branchcode = ?
1722 AND itemtype = ?', $branchcode, $itemtype],
1723 ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1724 FROM default_branch_circ_rules
1725 WHERE branchcode = ?', $branchcode],
1726 ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1727 FROM default_branch_item_rules
1728 WHERE itemtype = ?', $itemtype],
1729 ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1730 FROM default_circ_rules'],
1733 foreach my $attempt (@attempts) {
1734 my ($query, @bind_params) = @{$attempt};
1735 my $search_result = $dbh->selectrow_hashref ( $query , {}, @bind_params )
1736 or next;
1738 # Since branch/category and branch/itemtype use the same per-branch
1739 # defaults tables, we have to check that the key we want is set, not
1740 # just that a row was returned
1741 $result->{'holdallowed'} = $search_result->{'holdallowed'} unless ( defined $result->{'holdallowed'} );
1742 $result->{'hold_fulfillment_policy'} = $search_result->{'hold_fulfillment_policy'} unless ( defined $result->{'hold_fulfillment_policy'} );
1743 $result->{'returnbranch'} = $search_result->{'returnbranch'} unless ( defined $result->{'returnbranch'} );
1746 # built-in default circulation rule
1747 $result->{'holdallowed'} = 2 unless ( defined $result->{'holdallowed'} );
1748 $result->{'hold_fulfillment_policy'} = 'any' unless ( defined $result->{'hold_fulfillment_policy'} );
1749 $result->{'returnbranch'} = 'homebranch' unless ( defined $result->{'returnbranch'} );
1751 return $result;
1754 =head2 AddReturn
1756 ($doreturn, $messages, $iteminformation, $borrower) =
1757 &AddReturn( $barcode, $branch [,$exemptfine] [,$dropbox] [,$returndate] );
1759 Returns a book.
1761 =over 4
1763 =item C<$barcode> is the bar code of the book being returned.
1765 =item C<$branch> is the code of the branch where the book is being returned.
1767 =item C<$exemptfine> indicates that overdue charges for the item will be
1768 removed. Optional.
1770 =item C<$dropbox> indicates that the check-in date is assumed to be
1771 yesterday, or the last non-holiday as defined in C4::Calendar . If
1772 overdue charges are applied and C<$dropbox> is true, the last charge
1773 will be removed. This assumes that the fines accrual script has run
1774 for _today_. Optional.
1776 =item C<$return_date> allows the default return date to be overridden
1777 by the given return date. Optional.
1779 =back
1781 C<&AddReturn> returns a list of four items:
1783 C<$doreturn> is true iff the return succeeded.
1785 C<$messages> is a reference-to-hash giving feedback on the operation.
1786 The keys of the hash are:
1788 =over 4
1790 =item C<BadBarcode>
1792 No item with this barcode exists. The value is C<$barcode>.
1794 =item C<NotIssued>
1796 The book is not currently on loan. The value is C<$barcode>.
1798 =item C<IsPermanent>
1800 The book's home branch is a permanent collection. If you have borrowed
1801 this book, you are not allowed to return it. The value is the code for
1802 the book's home branch.
1804 =item C<withdrawn>
1806 This book has been withdrawn/cancelled. The value should be ignored.
1808 =item C<Wrongbranch>
1810 This book has was returned to the wrong branch. The value is a hashref
1811 so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
1812 contain the branchcode of the incorrect and correct return library, respectively.
1814 =item C<ResFound>
1816 The item was reserved. The value is a reference-to-hash whose keys are
1817 fields from the reserves table of the Koha database, and
1818 C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
1819 either C<Waiting>, C<Reserved>, or 0.
1821 =item C<WasReturned>
1823 Value 1 if return is successful.
1825 =item C<NeedsTransfer>
1827 If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer.
1829 =back
1831 C<$iteminformation> is a reference-to-hash, giving information about the
1832 returned item from the issues table.
1834 C<$borrower> is a reference-to-hash, giving information about the
1835 patron who last borrowed the book.
1837 =cut
1839 sub AddReturn {
1840 my ( $barcode, $branch, $exemptfine, $dropbox, $return_date, $dropboxdate ) = @_;
1842 if ($branch and not Koha::Libraries->find($branch)) {
1843 warn "AddReturn error: branch '$branch' not found. Reverting to " . C4::Context->userenv->{'branch'};
1844 undef $branch;
1846 $branch = C4::Context->userenv->{'branch'} unless $branch; # we trust userenv to be a safe fallback/default
1847 my $messages;
1848 my $borrower;
1849 my $biblio;
1850 my $doreturn = 1;
1851 my $validTransfert = 0;
1852 my $stat_type = 'return';
1854 # get information on item
1855 my $itemnumber = GetItemnumberFromBarcode( $barcode );
1856 unless ($itemnumber) {
1857 return (0, { BadBarcode => $barcode }); # no barcode means no item or borrower. bail out.
1859 my $issue = GetItemIssue($itemnumber);
1860 if ($issue and $issue->{borrowernumber}) {
1861 $borrower = C4::Members::GetMemberDetails($issue->{borrowernumber})
1862 or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existent borrowernumber '$issue->{borrowernumber}'\n"
1863 . Dumper($issue) . "\n";
1864 } else {
1865 $messages->{'NotIssued'} = $barcode;
1866 # even though item is not on loan, it may still be transferred; therefore, get current branch info
1867 $doreturn = 0;
1868 # No issue, no borrowernumber. ONLY if $doreturn, *might* you have a $borrower later.
1869 # Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
1870 if (C4::Context->preference("RecordLocalUseOnReturn")) {
1871 $messages->{'LocalUse'} = 1;
1872 $stat_type = 'localuse';
1876 my $item = GetItem($itemnumber) or die "GetItem($itemnumber) failed";
1878 if ( $item->{'location'} eq 'PROC' ) {
1879 if ( C4::Context->preference("InProcessingToShelvingCart") ) {
1880 $item->{'location'} = 'CART';
1882 else {
1883 $item->{location} = $item->{permanent_location};
1886 ModItem( $item, $item->{'biblionumber'}, $item->{'itemnumber'} );
1889 # full item data, but no borrowernumber or checkout info (no issue)
1890 # we know GetItem should work because GetItemnumberFromBarcode worked
1891 my $hbr = GetBranchItemRule($item->{'homebranch'}, $item->{'itype'})->{'returnbranch'} || "homebranch";
1892 # get the proper branch to which to return the item
1893 my $returnbranch = $item->{$hbr} || $branch ;
1894 # if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
1896 my $borrowernumber = $borrower->{'borrowernumber'} || undef; # we don't know if we had a borrower or not
1898 my $yaml = C4::Context->preference('UpdateNotForLoanStatusOnCheckin');
1899 if ($yaml) {
1900 $yaml = "$yaml\n\n"; # YAML is anal on ending \n. Surplus does not hurt
1901 my $rules;
1902 eval { $rules = YAML::Load($yaml); };
1903 if ($@) {
1904 warn "Unable to parse UpdateNotForLoanStatusOnCheckin syspref : $@";
1906 else {
1907 foreach my $key ( keys %$rules ) {
1908 if ( $item->{notforloan} eq $key ) {
1909 $messages->{'NotForLoanStatusUpdated'} = { from => $item->{notforloan}, to => $rules->{$key} };
1910 ModItem( { notforloan => $rules->{$key} }, undef, $itemnumber );
1911 last;
1918 # check if the book is in a permanent collection....
1919 # FIXME -- This 'PE' attribute is largely undocumented. afaict, there's no user interface that reflects this functionality.
1920 if ( $returnbranch ) {
1921 my $library = Koha::Libraries->find($returnbranch);
1922 if ( $library and $library->get_categories->search({'me.categorycode' => 'PE'})->count ) {
1923 $messages->{'IsPermanent'} = $returnbranch;
1927 # check if the return is allowed at this branch
1928 my ($returnallowed, $message) = CanBookBeReturned($item, $branch);
1929 unless ($returnallowed){
1930 $messages->{'Wrongbranch'} = {
1931 Wrongbranch => $branch,
1932 Rightbranch => $message
1934 $doreturn = 0;
1935 return ( $doreturn, $messages, $issue, $borrower );
1938 if ( $item->{'withdrawn'} ) { # book has been cancelled
1939 $messages->{'withdrawn'} = 1;
1940 $doreturn = 0 if C4::Context->preference("BlockReturnOfWithdrawnItems");
1943 # case of a return of document (deal with issues and holdingbranch)
1944 my $today = DateTime->now( time_zone => C4::Context->tz() );
1946 if ($doreturn) {
1947 my $datedue = $issue->{date_due};
1948 $borrower or warn "AddReturn without current borrower";
1949 my $circControlBranch;
1950 if ($dropbox) {
1951 # define circControlBranch only if dropbox mode is set
1952 # don't allow dropbox mode to create an invalid entry in issues (issuedate > today)
1953 # FIXME: check issuedate > returndate, factoring in holidays
1955 $circControlBranch = _GetCircControlBranch($item,$borrower);
1956 $issue->{'overdue'} = DateTime->compare($issue->{'date_due'}, $dropboxdate ) == -1 ? 1 : 0;
1959 if ($borrowernumber) {
1960 if ( ( C4::Context->preference('CalculateFinesOnReturn') && $issue->{'overdue'} ) || $return_date ) {
1961 _CalculateAndUpdateFine( { issue => $issue, item => $item, borrower => $borrower, return_date => $return_date } );
1964 eval {
1965 MarkIssueReturned( $borrowernumber, $item->{'itemnumber'},
1966 $circControlBranch, $return_date, $borrower->{'privacy'} );
1968 if ( $@ ) {
1969 $messages->{'Wrongbranch'} = {
1970 Wrongbranch => $branch,
1971 Rightbranch => $message
1973 carp $@;
1974 return ( 0, { WasReturned => 0 }, $issue, $borrower );
1977 # FIXME is the "= 1" right? This could be the borrower hash.
1978 $messages->{'WasReturned'} = 1;
1982 ModItem({ onloan => undef }, $issue->{'biblionumber'}, $item->{'itemnumber'});
1985 # the holdingbranch is updated if the document is returned to another location.
1986 # this is always done regardless of whether the item was on loan or not
1987 my $item_holding_branch = $item->{ holdingbranch };
1988 if ($item->{'holdingbranch'} ne $branch) {
1989 UpdateHoldingbranch($branch, $item->{'itemnumber'});
1990 $item->{'holdingbranch'} = $branch; # update item data holdingbranch too
1992 ModDateLastSeen( $item->{'itemnumber'} );
1994 # check if we have a transfer for this document
1995 my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->{'itemnumber'} );
1997 # if we have a transfer to do, we update the line of transfers with the datearrived
1998 my $is_in_rotating_collection = C4::RotatingCollections::isItemInAnyCollection( $item->{'itemnumber'} );
1999 if ($datesent) {
2000 if ( $tobranch eq $branch ) {
2001 my $sth = C4::Context->dbh->prepare(
2002 "UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL"
2004 $sth->execute( $item->{'itemnumber'} );
2005 # if we have a reservation with valid transfer, we can set it's status to 'W'
2006 ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
2007 C4::Reserves::ModReserveStatus($item->{'itemnumber'}, 'W');
2008 } else {
2009 $messages->{'WrongTransfer'} = $tobranch;
2010 $messages->{'WrongTransferItem'} = $item->{'itemnumber'};
2012 $validTransfert = 1;
2013 } else {
2014 ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
2017 # fix up the accounts.....
2018 if ( $item->{'itemlost'} ) {
2019 $messages->{'WasLost'} = 1;
2021 if ( $item->{'itemlost'} ) {
2022 if (
2023 Koha::RefundLostItemFeeRules->should_refund(
2025 current_branch => C4::Context->userenv->{branch},
2026 item_home_branch => $item->{homebranch},
2027 item_holding_branch => $item_holding_branch
2032 _FixAccountForLostAndReturned( $item->{'itemnumber'}, $borrowernumber, $barcode );
2033 $messages->{'LostItemFeeRefunded'} = 1;
2038 # fix up the overdues in accounts...
2039 if ($borrowernumber) {
2040 my $fix = _FixOverduesOnReturn($borrowernumber, $item->{itemnumber}, $exemptfine, $dropbox);
2041 defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $item->{itemnumber}...) failed!"; # zero is OK, check defined
2043 if ( $issue->{overdue} && $issue->{date_due} ) {
2044 # fix fine days
2045 $today = $dropboxdate if $dropbox;
2046 my ($debardate,$reminder) = _debar_user_on_return( $borrower, $item, $issue->{date_due}, $today );
2047 if ($reminder){
2048 $messages->{'PrevDebarred'} = $debardate;
2049 } else {
2050 $messages->{'Debarred'} = $debardate if $debardate;
2052 # there's no overdue on the item but borrower had been previously debarred
2053 } elsif ( $issue->{date_due} and $borrower->{'debarred'} ) {
2054 if ( $borrower->{debarred} eq "9999-12-31") {
2055 $messages->{'ForeverDebarred'} = $borrower->{'debarred'};
2056 } else {
2057 my $borrower_debar_dt = dt_from_string( $borrower->{debarred} );
2058 $borrower_debar_dt->truncate(to => 'day');
2059 my $today_dt = $today->clone()->truncate(to => 'day');
2060 if ( DateTime->compare( $borrower_debar_dt, $today_dt ) != -1 ) {
2061 $messages->{'PrevDebarred'} = $borrower->{'debarred'};
2067 # find reserves.....
2068 # if we don't have a reserve with the status W, we launch the Checkreserves routine
2069 my ($resfound, $resrec);
2070 my $lookahead= C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
2071 ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->{'itemnumber'}, undef, $lookahead ) unless ( $item->{'withdrawn'} );
2072 if ($resfound) {
2073 $resrec->{'ResFound'} = $resfound;
2074 $messages->{'ResFound'} = $resrec;
2077 # Record the fact that this book was returned.
2078 # FIXME itemtype should record item level type, not bibliolevel type
2079 UpdateStats({
2080 branch => $branch,
2081 type => $stat_type,
2082 itemnumber => $item->{'itemnumber'},
2083 itemtype => $biblio->{'itemtype'},
2084 borrowernumber => $borrowernumber,
2085 ccode => $item->{'ccode'}}
2088 # Send a check-in slip. # NOTE: borrower may be undef. probably shouldn't try to send messages then.
2089 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2090 my %conditions = (
2091 branchcode => $branch,
2092 categorycode => $borrower->{categorycode},
2093 item_type => $item->{itype},
2094 notification => 'CHECKIN',
2096 if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
2097 SendCirculationAlert({
2098 type => 'CHECKIN',
2099 item => $item,
2100 borrower => $borrower,
2101 branch => $branch,
2105 logaction("CIRCULATION", "RETURN", $borrowernumber, $item->{'itemnumber'})
2106 if C4::Context->preference("ReturnLog");
2108 # Remove any OVERDUES related debarment if the borrower has no overdues
2109 if ( $borrowernumber
2110 && $borrower->{'debarred'}
2111 && C4::Context->preference('AutoRemoveOverduesRestrictions')
2112 && !Koha::Patrons->find( $borrowernumber )->has_overdues
2113 && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2115 DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2118 # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
2119 if (!$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) and not $messages->{'WrongTransfer'}){
2120 if (C4::Context->preference("AutomaticItemReturn" ) or
2121 (C4::Context->preference("UseBranchTransferLimits") and
2122 ! IsBranchTransferAllowed($branch, $returnbranch, $item->{C4::Context->preference("BranchTransferLimitsType")} )
2123 )) {
2124 $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s)", $item->{'itemnumber'},$branch, $returnbranch;
2125 $debug and warn "item: " . Dumper($item);
2126 ModItemTransfer($item->{'itemnumber'}, $branch, $returnbranch);
2127 $messages->{'WasTransfered'} = 1;
2128 } else {
2129 $messages->{'NeedsTransfer'} = $returnbranch;
2133 return ( $doreturn, $messages, $issue, $borrower );
2136 =head2 MarkIssueReturned
2138 MarkIssueReturned($borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy);
2140 Unconditionally marks an issue as being returned by
2141 moving the C<issues> row to C<old_issues> and
2142 setting C<returndate> to the current date, or
2143 the last non-holiday date of the branccode specified in
2144 C<dropbox_branch> . Assumes you've already checked that
2145 it's safe to do this, i.e. last non-holiday > issuedate.
2147 if C<$returndate> is specified (in iso format), it is used as the date
2148 of the return. It is ignored when a dropbox_branch is passed in.
2150 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
2151 the old_issue is immediately anonymised
2153 Ideally, this function would be internal to C<C4::Circulation>,
2154 not exported, but it is currently needed by one
2155 routine in C<C4::Accounts>.
2157 =cut
2159 sub MarkIssueReturned {
2160 my ( $borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy ) = @_;
2162 my $anonymouspatron;
2163 if ( $privacy == 2 ) {
2164 # The default of 0 will not work due to foreign key constraints
2165 # The anonymisation will fail if AnonymousPatron is not a valid entry
2166 # We need to check if the anonymous patron exist, Koha will fail loudly if it does not
2167 # Note that a warning should appear on the about page (System information tab).
2168 $anonymouspatron = C4::Context->preference('AnonymousPatron');
2169 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."
2170 unless C4::Members::GetMember( borrowernumber => $anonymouspatron );
2172 my $dbh = C4::Context->dbh;
2173 my $query = 'UPDATE issues SET returndate=';
2174 my @bind;
2175 if ($dropbox_branch) {
2176 my $calendar = Koha::Calendar->new( branchcode => $dropbox_branch );
2177 my $dropboxdate = $calendar->addDate( DateTime->now( time_zone => C4::Context->tz), -1 );
2178 $query .= ' ? ';
2179 push @bind, $dropboxdate->strftime('%Y-%m-%d %H:%M');
2180 } elsif ($returndate) {
2181 $query .= ' ? ';
2182 push @bind, $returndate;
2183 } else {
2184 $query .= ' now() ';
2186 $query .= ' WHERE borrowernumber = ? AND itemnumber = ?';
2187 push @bind, $borrowernumber, $itemnumber;
2188 # FIXME transaction
2189 my $sth_upd = $dbh->prepare($query);
2190 $sth_upd->execute(@bind);
2191 my $sth_copy = $dbh->prepare('INSERT INTO old_issues SELECT * FROM issues
2192 WHERE borrowernumber = ?
2193 AND itemnumber = ?');
2194 $sth_copy->execute($borrowernumber, $itemnumber);
2195 # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
2196 if ( $privacy == 2) {
2197 my $sth_ano = $dbh->prepare("UPDATE old_issues SET borrowernumber=?
2198 WHERE borrowernumber = ?
2199 AND itemnumber = ?");
2200 $sth_ano->execute($anonymouspatron, $borrowernumber, $itemnumber);
2202 my $sth_del = $dbh->prepare("DELETE FROM issues
2203 WHERE borrowernumber = ?
2204 AND itemnumber = ?");
2205 $sth_del->execute($borrowernumber, $itemnumber);
2207 ModItem( { 'onloan' => undef }, undef, $itemnumber );
2209 if ( C4::Context->preference('StoreLastBorrower') ) {
2210 my $item = Koha::Items->find( $itemnumber );
2211 my $patron = Koha::Patrons->find( $borrowernumber );
2212 $item->last_returned_by( $patron );
2216 =head2 _debar_user_on_return
2218 _debar_user_on_return($borrower, $item, $datedue, today);
2220 C<$borrower> borrower hashref
2222 C<$item> item hashref
2224 C<$datedue> date due DateTime object
2226 C<$today> DateTime object representing the return time
2228 Internal function, called only by AddReturn that calculates and updates
2229 the user fine days, and debars him if necessary.
2231 Should only be called for overdue returns
2233 =cut
2235 sub _debar_user_on_return {
2236 my ( $borrower, $item, $dt_due, $dt_today ) = @_;
2238 my $branchcode = _GetCircControlBranch( $item, $borrower );
2240 my $circcontrol = C4::Context->preference('CircControl');
2241 my $issuingrule =
2242 GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode );
2243 my $finedays = $issuingrule->{finedays};
2244 my $unit = $issuingrule->{lengthunit};
2245 my $chargeable_units = C4::Overdues::get_chargeable_units($unit, $dt_due, $dt_today, $branchcode);
2247 if ($finedays) {
2249 # finedays is in days, so hourly loans must multiply by 24
2250 # thus 1 hour late equals 1 day suspension * finedays rate
2251 $finedays = $finedays * 24 if ( $unit eq 'hours' );
2253 # grace period is measured in the same units as the loan
2254 my $grace =
2255 DateTime::Duration->new( $unit => $issuingrule->{firstremind} );
2257 my $deltadays = DateTime::Duration->new(
2258 days => $chargeable_units
2260 if ( $deltadays->subtract($grace)->is_positive() ) {
2261 my $suspension_days = $deltadays * $finedays;
2263 # If the max suspension days is < than the suspension days
2264 # the suspension days is limited to this maximum period.
2265 my $max_sd = $issuingrule->{maxsuspensiondays};
2266 if ( defined $max_sd ) {
2267 $max_sd = DateTime::Duration->new( days => $max_sd );
2268 $suspension_days = $max_sd
2269 if DateTime::Duration->compare( $max_sd, $suspension_days ) < 0;
2272 my $new_debar_dt =
2273 $dt_today->clone()->add_duration( $suspension_days );
2275 Koha::Patron::Debarments::AddUniqueDebarment({
2276 borrowernumber => $borrower->{borrowernumber},
2277 expiration => $new_debar_dt->ymd(),
2278 type => 'SUSPENSION',
2280 # if borrower was already debarred but does not get an extra debarment
2281 my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
2282 if ( $borrower->{debarred} eq $patron->is_debarred ) {
2283 return ($borrower->{debarred},1);
2285 return $new_debar_dt->ymd();
2288 return;
2291 =head2 _FixOverduesOnReturn
2293 &_FixOverduesOnReturn($brn,$itm, $exemptfine, $dropboxmode);
2295 C<$brn> borrowernumber
2297 C<$itm> itemnumber
2299 C<$exemptfine> BOOL -- remove overdue charge associated with this issue.
2300 C<$dropboxmode> BOOL -- remove lastincrement on overdue charge associated with this issue.
2302 Internal function, called only by AddReturn
2304 =cut
2306 sub _FixOverduesOnReturn {
2307 my ($borrowernumber, $item);
2308 unless ($borrowernumber = shift) {
2309 warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
2310 return;
2312 unless ($item = shift) {
2313 warn "_FixOverduesOnReturn() not supplied valid itemnumber";
2314 return;
2316 my ($exemptfine, $dropbox) = @_;
2317 my $dbh = C4::Context->dbh;
2319 # check for overdue fine
2320 my $sth = $dbh->prepare(
2321 "SELECT * FROM accountlines WHERE (borrowernumber = ?) AND (itemnumber = ?) AND (accounttype='FU' OR accounttype='O')"
2323 $sth->execute( $borrowernumber, $item );
2325 # alter fine to show that the book has been returned
2326 my $data = $sth->fetchrow_hashref;
2327 return 0 unless $data; # no warning, there's just nothing to fix
2329 my $uquery;
2330 my @bind = ($data->{'accountlines_id'});
2331 if ($exemptfine) {
2332 $uquery = "update accountlines set accounttype='FFOR', amountoutstanding=0";
2333 if (C4::Context->preference("FinesLog")) {
2334 &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
2336 } elsif ($dropbox && $data->{lastincrement}) {
2337 my $outstanding = $data->{amountoutstanding} - $data->{lastincrement} ;
2338 my $amt = $data->{amount} - $data->{lastincrement} ;
2339 if (C4::Context->preference("FinesLog")) {
2340 &logaction("FINES", 'MODIFY',$borrowernumber,"Dropbox adjustment $amt, item $item");
2342 $uquery = "update accountlines set accounttype='F' ";
2343 if($outstanding >= 0 && $amt >=0) {
2344 $uquery .= ", amount = ? , amountoutstanding=? ";
2345 unshift @bind, ($amt, $outstanding) ;
2347 } else {
2348 $uquery = "update accountlines set accounttype='F' ";
2350 $uquery .= " where (accountlines_id = ?)";
2351 my $usth = $dbh->prepare($uquery);
2352 return $usth->execute(@bind);
2355 =head2 _FixAccountForLostAndReturned
2357 &_FixAccountForLostAndReturned($itemnumber, [$borrowernumber, $barcode]);
2359 Calculates the charge for a book lost and returned.
2361 Internal function, not exported, called only by AddReturn.
2363 FIXME: This function reflects how inscrutable fines logic is. Fix both.
2364 FIXME: Give a positive return value on success. It might be the $borrowernumber who received credit, or the amount forgiven.
2366 =cut
2368 sub _FixAccountForLostAndReturned {
2369 my $itemnumber = shift or return;
2370 my $borrowernumber = @_ ? shift : undef;
2371 my $item_id = @_ ? shift : $itemnumber; # Send the barcode if you want that logged in the description
2372 my $dbh = C4::Context->dbh;
2373 # check for charge made for lost book
2374 my $sth = $dbh->prepare("SELECT * FROM accountlines WHERE itemnumber = ? AND accounttype IN ('L', 'Rep', 'W') ORDER BY date DESC, accountno DESC");
2375 $sth->execute($itemnumber);
2376 my $data = $sth->fetchrow_hashref;
2377 $data or return; # bail if there is nothing to do
2378 $data->{accounttype} eq 'W' and return; # Written off
2380 # writeoff this amount
2381 my $offset;
2382 my $amount = $data->{'amount'};
2383 my $acctno = $data->{'accountno'};
2384 my $amountleft; # Starts off undef/zero.
2385 if ($data->{'amountoutstanding'} == $amount) {
2386 $offset = $data->{'amount'};
2387 $amountleft = 0; # Hey, it's zero here, too.
2388 } else {
2389 $offset = $amount - $data->{'amountoutstanding'}; # Um, isn't this the same as ZERO? We just tested those two things are ==
2390 $amountleft = $data->{'amountoutstanding'} - $amount; # Um, isn't this the same as ZERO? We just tested those two things are ==
2392 my $usth = $dbh->prepare("UPDATE accountlines SET accounttype = 'LR',amountoutstanding='0'
2393 WHERE (accountlines_id = ?)");
2394 $usth->execute($data->{'accountlines_id'}); # We might be adjusting an account for some OTHER borrowernumber now. Not the one we passed in.
2395 #check if any credit is left if so writeoff other accounts
2396 my $nextaccntno = getnextacctno($data->{'borrowernumber'});
2397 $amountleft *= -1 if ($amountleft < 0);
2398 if ($amountleft > 0) {
2399 my $msth = $dbh->prepare("SELECT * FROM accountlines WHERE (borrowernumber = ?)
2400 AND (amountoutstanding >0) ORDER BY date"); # might want to order by amountoustanding ASC (pay smallest first)
2401 $msth->execute($data->{'borrowernumber'});
2402 # offset transactions
2403 my $newamtos;
2404 my $accdata;
2405 while (($accdata=$msth->fetchrow_hashref) and ($amountleft>0)){
2406 if ($accdata->{'amountoutstanding'} < $amountleft) {
2407 $newamtos = 0;
2408 $amountleft -= $accdata->{'amountoutstanding'};
2409 } else {
2410 $newamtos = $accdata->{'amountoutstanding'} - $amountleft;
2411 $amountleft = 0;
2413 my $thisacct = $accdata->{'accountlines_id'};
2414 # FIXME: move prepares outside while loop!
2415 my $usth = $dbh->prepare("UPDATE accountlines SET amountoutstanding= ?
2416 WHERE (accountlines_id = ?)");
2417 $usth->execute($newamtos,$thisacct);
2418 $usth = $dbh->prepare("INSERT INTO accountoffsets
2419 (borrowernumber, accountno, offsetaccount, offsetamount)
2420 VALUES
2421 (?,?,?,?)");
2422 $usth->execute($data->{'borrowernumber'},$accdata->{'accountno'},$nextaccntno,$newamtos);
2425 $amountleft *= -1 if ($amountleft > 0);
2426 my $desc = "Item Returned " . $item_id;
2427 $usth = $dbh->prepare("INSERT INTO accountlines
2428 (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
2429 VALUES (?,?,now(),?,?,'CR',?)");
2430 $usth->execute($data->{'borrowernumber'},$nextaccntno,0-$amount,$desc,$amountleft);
2431 if ($borrowernumber) {
2432 # FIXME: same as query above. use 1 sth for both
2433 $usth = $dbh->prepare("INSERT INTO accountoffsets
2434 (borrowernumber, accountno, offsetaccount, offsetamount)
2435 VALUES (?,?,?,?)");
2436 $usth->execute($borrowernumber, $data->{'accountno'}, $nextaccntno, $offset);
2438 ModItem({ paidfor => '' }, undef, $itemnumber);
2439 return;
2442 =head2 _GetCircControlBranch
2444 my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
2446 Internal function :
2448 Return the library code to be used to determine which circulation
2449 policy applies to a transaction. Looks up the CircControl and
2450 HomeOrHoldingBranch system preferences.
2452 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
2454 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
2456 =cut
2458 sub _GetCircControlBranch {
2459 my ($item, $borrower) = @_;
2460 my $circcontrol = C4::Context->preference('CircControl');
2461 my $branch;
2463 if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
2464 $branch= C4::Context->userenv->{'branch'};
2465 } elsif ($circcontrol eq 'PatronLibrary') {
2466 $branch=$borrower->{branchcode};
2467 } else {
2468 my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
2469 $branch = $item->{$branchfield};
2470 # default to item home branch if holdingbranch is used
2471 # and is not defined
2472 if (!defined($branch) && $branchfield eq 'holdingbranch') {
2473 $branch = $item->{homebranch};
2476 return $branch;
2484 =head2 GetItemIssue
2486 $issue = &GetItemIssue($itemnumber);
2488 Returns patron currently having a book, or undef if not checked out.
2490 C<$itemnumber> is the itemnumber.
2492 C<$issue> is a hashref of the row from the issues table.
2494 =cut
2496 sub GetItemIssue {
2497 my ($itemnumber) = @_;
2498 return unless $itemnumber;
2499 my $sth = C4::Context->dbh->prepare(
2500 "SELECT items.*, issues.*
2501 FROM issues
2502 LEFT JOIN items ON issues.itemnumber=items.itemnumber
2503 WHERE issues.itemnumber=?");
2504 $sth->execute($itemnumber);
2505 my $data = $sth->fetchrow_hashref;
2506 return unless $data;
2507 $data->{issuedate_sql} = $data->{issuedate};
2508 $data->{date_due_sql} = $data->{date_due};
2509 $data->{issuedate} = dt_from_string($data->{issuedate}, 'sql');
2510 $data->{issuedate}->truncate(to => 'minute');
2511 $data->{date_due} = dt_from_string($data->{date_due}, 'sql');
2512 $data->{date_due}->truncate(to => 'minute');
2513 my $dt = DateTime->now( time_zone => C4::Context->tz)->truncate( to => 'minute');
2514 $data->{'overdue'} = DateTime->compare($data->{'date_due'}, $dt ) == -1 ? 1 : 0;
2515 return $data;
2518 =head2 GetOpenIssue
2520 $issue = GetOpenIssue( $itemnumber );
2522 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2524 C<$itemnumber> is the item's itemnumber
2526 Returns a hashref
2528 =cut
2530 sub GetOpenIssue {
2531 my ( $itemnumber ) = @_;
2532 return unless $itemnumber;
2533 my $dbh = C4::Context->dbh;
2534 my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2535 $sth->execute( $itemnumber );
2536 return $sth->fetchrow_hashref();
2540 =head2 GetIssues
2542 $issues = GetIssues({}); # return all issues!
2543 $issues = GetIssues({ borrowernumber => $borrowernumber, biblionumber => $biblionumber });
2545 Returns all pending issues that match given criteria.
2546 Returns a arrayref or undef if an error occurs.
2548 Allowed criteria are:
2550 =over 2
2552 =item * borrowernumber
2554 =item * biblionumber
2556 =item * itemnumber
2558 =back
2560 =cut
2562 sub GetIssues {
2563 my ($criteria) = @_;
2565 # Build filters
2566 my @filters;
2567 my @allowed = qw(borrowernumber biblionumber itemnumber);
2568 foreach (@allowed) {
2569 if (defined $criteria->{$_}) {
2570 push @filters, {
2571 field => $_,
2572 value => $criteria->{$_},
2577 # Do we need to join other tables ?
2578 my %join;
2579 if (defined $criteria->{biblionumber}) {
2580 $join{items} = 1;
2583 # Build SQL query
2584 my $where = '';
2585 if (@filters) {
2586 $where = "WHERE " . join(' AND ', map { "$_->{field} = ?" } @filters);
2588 my $query = q{
2589 SELECT issues.*
2590 FROM issues
2592 if (defined $join{items}) {
2593 $query .= q{
2594 LEFT JOIN items ON (issues.itemnumber = items.itemnumber)
2597 $query .= $where;
2599 # Execute SQL query
2600 my $dbh = C4::Context->dbh;
2601 my $sth = $dbh->prepare($query);
2602 my $rv = $sth->execute(map { $_->{value} } @filters);
2604 return $rv ? $sth->fetchall_arrayref({}) : undef;
2607 =head2 GetItemIssues
2609 $issues = &GetItemIssues($itemnumber, $history);
2611 Returns patrons that have issued a book
2613 C<$itemnumber> is the itemnumber
2614 C<$history> is false if you just want the current "issuer" (if any)
2615 and true if you want issues history from old_issues also.
2617 Returns reference to an array of hashes
2619 =cut
2621 sub GetItemIssues {
2622 my ( $itemnumber, $history ) = @_;
2624 my $today = DateTime->now( time_zome => C4::Context->tz); # get today date
2625 $today->truncate( to => 'minute' );
2626 my $sql = "SELECT * FROM issues
2627 JOIN borrowers USING (borrowernumber)
2628 JOIN items USING (itemnumber)
2629 WHERE issues.itemnumber = ? ";
2630 if ($history) {
2631 $sql .= "UNION ALL
2632 SELECT * FROM old_issues
2633 LEFT JOIN borrowers USING (borrowernumber)
2634 JOIN items USING (itemnumber)
2635 WHERE old_issues.itemnumber = ? ";
2637 $sql .= "ORDER BY date_due DESC";
2638 my $sth = C4::Context->dbh->prepare($sql);
2639 if ($history) {
2640 $sth->execute($itemnumber, $itemnumber);
2641 } else {
2642 $sth->execute($itemnumber);
2644 my $results = $sth->fetchall_arrayref({});
2645 foreach (@$results) {
2646 my $date_due = dt_from_string($_->{date_due},'sql');
2647 $date_due->truncate( to => 'minute' );
2649 $_->{overdue} = (DateTime->compare($date_due, $today) == -1) ? 1 : 0;
2651 return $results;
2654 =head2 GetBiblioIssues
2656 $issues = GetBiblioIssues($biblionumber);
2658 this function get all issues from a biblionumber.
2660 Return:
2661 C<$issues> is a reference to array which each value is ref-to-hash. This ref-to-hash containts all column from
2662 tables issues and the firstname,surname & cardnumber from borrowers.
2664 =cut
2666 sub GetBiblioIssues {
2667 my $biblionumber = shift;
2668 return unless $biblionumber;
2669 my $dbh = C4::Context->dbh;
2670 my $query = "
2671 SELECT issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2672 FROM issues
2673 LEFT JOIN borrowers ON borrowers.borrowernumber = issues.borrowernumber
2674 LEFT JOIN items ON issues.itemnumber = items.itemnumber
2675 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2676 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2677 WHERE biblio.biblionumber = ?
2678 UNION ALL
2679 SELECT old_issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2680 FROM old_issues
2681 LEFT JOIN borrowers ON borrowers.borrowernumber = old_issues.borrowernumber
2682 LEFT JOIN items ON old_issues.itemnumber = items.itemnumber
2683 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2684 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2685 WHERE biblio.biblionumber = ?
2686 ORDER BY timestamp
2688 my $sth = $dbh->prepare($query);
2689 $sth->execute($biblionumber, $biblionumber);
2691 my @issues;
2692 while ( my $data = $sth->fetchrow_hashref ) {
2693 push @issues, $data;
2695 return \@issues;
2698 =head2 GetUpcomingDueIssues
2700 my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2702 =cut
2704 sub GetUpcomingDueIssues {
2705 my $params = shift;
2707 $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2708 my $dbh = C4::Context->dbh;
2710 my $statement = <<END_SQL;
2711 SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2712 FROM issues
2713 LEFT JOIN items USING (itemnumber)
2714 LEFT OUTER JOIN branches USING (branchcode)
2715 WHERE returndate is NULL
2716 HAVING days_until_due >= 0 AND days_until_due <= ?
2717 END_SQL
2719 my @bind_parameters = ( $params->{'days_in_advance'} );
2721 my $sth = $dbh->prepare( $statement );
2722 $sth->execute( @bind_parameters );
2723 my $upcoming_dues = $sth->fetchall_arrayref({});
2725 return $upcoming_dues;
2728 =head2 CanBookBeRenewed
2730 ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber[, $override_limit]);
2732 Find out whether a borrowed item may be renewed.
2734 C<$borrowernumber> is the borrower number of the patron who currently
2735 has the item on loan.
2737 C<$itemnumber> is the number of the item to renew.
2739 C<$override_limit>, if supplied with a true value, causes
2740 the limit on the number of times that the loan can be renewed
2741 (as controlled by the item type) to be ignored. Overriding also allows
2742 to renew sooner than "No renewal before" and to manually renew loans
2743 that are automatically renewed.
2745 C<$CanBookBeRenewed> returns a true value if the item may be renewed. The
2746 item must currently be on loan to the specified borrower; renewals
2747 must be allowed for the item's type; and the borrower must not have
2748 already renewed the loan. $error will contain the reason the renewal can not proceed
2750 =cut
2752 sub CanBookBeRenewed {
2753 my ( $borrowernumber, $itemnumber, $override_limit ) = @_;
2755 my $dbh = C4::Context->dbh;
2756 my $renews = 1;
2758 my $item = GetItem($itemnumber) or return ( 0, 'no_item' );
2759 my $itemissue = GetItemIssue($itemnumber) or return ( 0, 'no_checkout' );
2760 return ( 0, 'onsite_checkout' ) if $itemissue->{onsite_checkout};
2762 $borrowernumber ||= $itemissue->{borrowernumber};
2763 my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber )
2764 or return;
2766 my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2768 # This item can fill one or more unfilled reserve, can those unfilled reserves
2769 # all be filled by other available items?
2770 if ( $resfound
2771 && C4::Context->preference('AllowRenewalIfOtherItemsAvailable') )
2773 my $schema = Koha::Database->new()->schema();
2775 my $item_holds = $schema->resultset('Reserve')->search( { itemnumber => $itemnumber, found => undef } )->count();
2776 if ($item_holds) {
2777 # There is an item level hold on this item, no other item can fill the hold
2778 $resfound = 1;
2780 else {
2782 # Get all other items that could possibly fill reserves
2783 my @itemnumbers = $schema->resultset('Item')->search(
2785 biblionumber => $resrec->{biblionumber},
2786 onloan => undef,
2787 notforloan => 0,
2788 -not => { itemnumber => $itemnumber }
2790 { columns => 'itemnumber' }
2791 )->get_column('itemnumber')->all();
2793 # Get all other reserves that could have been filled by this item
2794 my @borrowernumbers;
2795 while (1) {
2796 my ( $reserve_found, $reserve, undef ) =
2797 C4::Reserves::CheckReserves( $itemnumber, undef, undef, \@borrowernumbers );
2799 if ($reserve_found) {
2800 push( @borrowernumbers, $reserve->{borrowernumber} );
2802 else {
2803 last;
2807 # If the count of the union of the lists of reservable items for each borrower
2808 # is equal or greater than the number of borrowers, we know that all reserves
2809 # can be filled with available items. We can get the union of the sets simply
2810 # by pushing all the elements onto an array and removing the duplicates.
2811 my @reservable;
2812 foreach my $b (@borrowernumbers) {
2813 my ($borr) = C4::Members::GetMemberDetails($b);
2814 foreach my $i (@itemnumbers) {
2815 my $item = GetItem($i);
2816 if ( IsAvailableForItemLevelRequest( $item, $borr )
2817 && CanItemBeReserved( $b, $i )
2818 && !IsItemOnHoldAndFound($i) )
2820 push( @reservable, $i );
2825 @reservable = uniq(@reservable);
2827 if ( @reservable >= @borrowernumbers ) {
2828 $resfound = 0;
2832 return ( 0, "on_reserve" ) if $resfound; # '' when no hold was found
2834 return ( 1, undef ) if $override_limit;
2836 my $branchcode = _GetCircControlBranch( $item, $borrower );
2837 my $issuingrule =
2838 GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode );
2840 return ( 0, "too_many" )
2841 if $issuingrule->{renewalsallowed} <= $itemissue->{renewals};
2843 my $overduesblockrenewing = C4::Context->preference('OverduesBlockRenewing');
2844 my $restrictionblockrenewing = C4::Context->preference('RestrictionBlockRenewing');
2845 my $patron = Koha::Patrons->find($borrowernumber);
2846 my $restricted = $patron->is_debarred;
2847 my $hasoverdues = $patron->has_overdues;
2849 if ( $restricted and $restrictionblockrenewing ) {
2850 return ( 0, 'restriction');
2851 } elsif ( ($hasoverdues and $overduesblockrenewing eq 'block') || ($itemissue->{overdue} and $overduesblockrenewing eq 'blockitem') ) {
2852 return ( 0, 'overdue');
2855 if ( defined $issuingrule->{norenewalbefore}
2856 and $issuingrule->{norenewalbefore} ne "" )
2859 # Calculate soonest renewal by subtracting 'No renewal before' from due date
2860 my $soonestrenewal =
2861 $itemissue->{date_due}->clone()
2862 ->subtract(
2863 $issuingrule->{lengthunit} => $issuingrule->{norenewalbefore} );
2865 # Depending on syspref reset the exact time, only check the date
2866 if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
2867 and $issuingrule->{lengthunit} eq 'days' )
2869 $soonestrenewal->truncate( to => 'day' );
2872 if ( $soonestrenewal > DateTime->now( time_zone => C4::Context->tz() ) )
2874 return ( 0, "auto_too_soon" ) if $itemissue->{auto_renew};
2875 return ( 0, "too_soon" );
2877 elsif ( $itemissue->{auto_renew} ) {
2878 return ( 0, "auto_renew" );
2882 # Fallback for automatic renewals:
2883 # If norenewalbefore is undef, don't renew before due date.
2884 elsif ( $itemissue->{auto_renew} ) {
2885 my $now = dt_from_string;
2886 return ( 0, "auto_renew" )
2887 if $now >= $itemissue->{date_due};
2888 return ( 0, "auto_too_soon" );
2891 return ( 1, undef );
2894 =head2 AddRenewal
2896 &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
2898 Renews a loan.
2900 C<$borrowernumber> is the borrower number of the patron who currently
2901 has the item.
2903 C<$itemnumber> is the number of the item to renew.
2905 C<$branch> is the library where the renewal took place (if any).
2906 The library that controls the circ policies for the renewal is retrieved from the issues record.
2908 C<$datedue> can be a DateTime object used to set the due date.
2910 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate. If
2911 this parameter is not supplied, lastreneweddate is set to the current date.
2913 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2914 from the book's item type.
2916 =cut
2918 sub AddRenewal {
2919 my $borrowernumber = shift;
2920 my $itemnumber = shift or return;
2921 my $branch = shift;
2922 my $datedue = shift;
2923 my $lastreneweddate = shift || DateTime->now(time_zone => C4::Context->tz)->ymd();
2925 my $item = GetItem($itemnumber) or return;
2926 my $biblio = GetBiblioFromItemNumber($itemnumber) or return;
2928 my $dbh = C4::Context->dbh;
2930 # Find the issues record for this book
2931 my $issuedata = GetItemIssue($itemnumber);
2933 return unless ( $issuedata );
2935 $borrowernumber ||= $issuedata->{borrowernumber};
2937 if ( defined $datedue && ref $datedue ne 'DateTime' ) {
2938 carp 'Invalid date passed to AddRenewal.';
2939 return;
2942 my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber ) or return;
2944 if ( C4::Context->preference('CalculateFinesOnReturn') && $issuedata->{overdue} ) {
2945 _CalculateAndUpdateFine( { issue => $issuedata, item => $item, borrower => $borrower } );
2947 _FixOverduesOnReturn( $borrowernumber, $itemnumber );
2949 # If the due date wasn't specified, calculate it by adding the
2950 # book's loan length to today's date or the current due date
2951 # based on the value of the RenewalPeriodBase syspref.
2952 unless ($datedue) {
2954 my $itemtype = (C4::Context->preference('item-level_itypes')) ? $biblio->{'itype'} : $biblio->{'itemtype'};
2956 $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
2957 dt_from_string( $issuedata->{date_due} ) :
2958 DateTime->now( time_zone => C4::Context->tz());
2959 $datedue = CalcDateDue($datedue, $itemtype, $issuedata->{'branchcode'}, $borrower, 'is a renewal');
2962 # Update the issues record to have the new due date, and a new count
2963 # of how many times it has been renewed.
2964 my $renews = $issuedata->{'renewals'} + 1;
2965 my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
2966 WHERE borrowernumber=?
2967 AND itemnumber=?"
2970 $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
2972 # Update the renewal count on the item, and tell zebra to reindex
2973 $renews = $biblio->{'renewals'} + 1;
2974 ModItem({ renewals => $renews, onloan => $datedue->strftime('%Y-%m-%d %H:%M')}, $biblio->{'biblionumber'}, $itemnumber);
2976 # Charge a new rental fee, if applicable?
2977 my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
2978 if ( $charge > 0 ) {
2979 my $accountno = getnextacctno( $borrowernumber );
2980 my $item = GetBiblioFromItemNumber($itemnumber);
2981 my $manager_id = 0;
2982 $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
2983 $sth = $dbh->prepare(
2984 "INSERT INTO accountlines
2985 (date, borrowernumber, accountno, amount, manager_id,
2986 description,accounttype, amountoutstanding, itemnumber)
2987 VALUES (now(),?,?,?,?,?,?,?,?)"
2989 $sth->execute( $borrowernumber, $accountno, $charge, $manager_id,
2990 "Renewal of Rental Item $item->{'title'} $item->{'barcode'}",
2991 'Rent', $charge, $itemnumber );
2994 # Send a renewal slip according to checkout alert preferencei
2995 if ( C4::Context->preference('RenewalSendNotice') eq '1' ) {
2996 $borrower = C4::Members::GetMemberDetails( $borrowernumber, 0 );
2997 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2998 my %conditions = (
2999 branchcode => $branch,
3000 categorycode => $borrower->{categorycode},
3001 item_type => $item->{itype},
3002 notification => 'CHECKOUT',
3004 if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
3005 SendCirculationAlert(
3007 type => 'RENEWAL',
3008 item => $item,
3009 borrower => $borrower,
3010 branch => $branch,
3016 # Remove any OVERDUES related debarment if the borrower has no overdues
3017 $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber );
3018 if ( $borrowernumber
3019 && $borrower->{'debarred'}
3020 && !Koha::Patrons->find( $borrowernumber )->has_overdues
3021 && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
3023 DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
3026 # Log the renewal
3027 UpdateStats({branch => $branch,
3028 type => 'renew',
3029 amount => $charge,
3030 itemnumber => $itemnumber,
3031 itemtype => $item->{itype},
3032 borrowernumber => $borrowernumber,
3033 ccode => $item->{'ccode'}}
3035 return $datedue;
3038 sub GetRenewCount {
3039 # check renewal status
3040 my ( $bornum, $itemno ) = @_;
3041 my $dbh = C4::Context->dbh;
3042 my $renewcount = 0;
3043 my $renewsallowed = 0;
3044 my $renewsleft = 0;
3046 my $borrower = C4::Members::GetMember( borrowernumber => $bornum);
3047 my $item = GetItem($itemno);
3049 # Look in the issues table for this item, lent to this borrower,
3050 # and not yet returned.
3052 # FIXME - I think this function could be redone to use only one SQL call.
3053 my $sth = $dbh->prepare(
3054 "select * from issues
3055 where (borrowernumber = ?)
3056 and (itemnumber = ?)"
3058 $sth->execute( $bornum, $itemno );
3059 my $data = $sth->fetchrow_hashref;
3060 $renewcount = $data->{'renewals'} if $data->{'renewals'};
3061 # $item and $borrower should be calculated
3062 my $branchcode = _GetCircControlBranch($item, $borrower);
3064 my $issuingrule = GetIssuingRule($borrower->{categorycode}, $item->{itype}, $branchcode);
3066 $renewsallowed = $issuingrule->{'renewalsallowed'};
3067 $renewsleft = $renewsallowed - $renewcount;
3068 if($renewsleft < 0){ $renewsleft = 0; }
3069 return ( $renewcount, $renewsallowed, $renewsleft );
3072 =head2 GetSoonestRenewDate
3074 $NoRenewalBeforeThisDate = &GetSoonestRenewDate($borrowernumber, $itemnumber);
3076 Find out the soonest possible renew date of a borrowed item.
3078 C<$borrowernumber> is the borrower number of the patron who currently
3079 has the item on loan.
3081 C<$itemnumber> is the number of the item to renew.
3083 C<$GetSoonestRenewDate> returns the DateTime of the soonest possible
3084 renew date, based on the value "No renewal before" of the applicable
3085 issuing rule. Returns the current date if the item can already be
3086 renewed, and returns undefined if the borrower, loan, or item
3087 cannot be found.
3089 =cut
3091 sub GetSoonestRenewDate {
3092 my ( $borrowernumber, $itemnumber ) = @_;
3094 my $dbh = C4::Context->dbh;
3096 my $item = GetItem($itemnumber) or return;
3097 my $itemissue = GetItemIssue($itemnumber) or return;
3099 $borrowernumber ||= $itemissue->{borrowernumber};
3100 my $borrower = C4::Members::GetMemberDetails($borrowernumber)
3101 or return;
3103 my $branchcode = _GetCircControlBranch( $item, $borrower );
3104 my $issuingrule =
3105 GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode );
3107 my $now = dt_from_string;
3109 if ( defined $issuingrule->{norenewalbefore}
3110 and $issuingrule->{norenewalbefore} ne "" )
3112 my $soonestrenewal =
3113 $itemissue->{date_due}->clone()
3114 ->subtract(
3115 $issuingrule->{lengthunit} => $issuingrule->{norenewalbefore} );
3117 if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
3118 and $issuingrule->{lengthunit} eq 'days' )
3120 $soonestrenewal->truncate( to => 'day' );
3122 return $soonestrenewal if $now < $soonestrenewal;
3124 return $now;
3127 =head2 GetIssuingCharges
3129 ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
3131 Calculate how much it would cost for a given patron to borrow a given
3132 item, including any applicable discounts.
3134 C<$itemnumber> is the item number of item the patron wishes to borrow.
3136 C<$borrowernumber> is the patron's borrower number.
3138 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
3139 and C<$item_type> is the code for the item's item type (e.g., C<VID>
3140 if it's a video).
3142 =cut
3144 sub GetIssuingCharges {
3146 # calculate charges due
3147 my ( $itemnumber, $borrowernumber ) = @_;
3148 my $charge = 0;
3149 my $dbh = C4::Context->dbh;
3150 my $item_type;
3152 # Get the book's item type and rental charge (via its biblioitem).
3153 my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
3154 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
3155 $charge_query .= (C4::Context->preference('item-level_itypes'))
3156 ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
3157 : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
3159 $charge_query .= ' WHERE items.itemnumber =?';
3161 my $sth = $dbh->prepare($charge_query);
3162 $sth->execute($itemnumber);
3163 if ( my $item_data = $sth->fetchrow_hashref ) {
3164 $item_type = $item_data->{itemtype};
3165 $charge = $item_data->{rentalcharge};
3166 my $branch = C4::Context::mybranch();
3167 my $discount_query = q|SELECT rentaldiscount,
3168 issuingrules.itemtype, issuingrules.branchcode
3169 FROM borrowers
3170 LEFT JOIN issuingrules ON borrowers.categorycode = issuingrules.categorycode
3171 WHERE borrowers.borrowernumber = ?
3172 AND (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
3173 AND (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')|;
3174 my $discount_sth = $dbh->prepare($discount_query);
3175 $discount_sth->execute( $borrowernumber, $item_type, $branch );
3176 my $discount_rules = $discount_sth->fetchall_arrayref({});
3177 if (@{$discount_rules}) {
3178 # We may have multiple rules so get the most specific
3179 my $discount = _get_discount_from_rule($discount_rules, $branch, $item_type);
3180 $charge = ( $charge * ( 100 - $discount ) ) / 100;
3184 return ( $charge, $item_type );
3187 # Select most appropriate discount rule from those returned
3188 sub _get_discount_from_rule {
3189 my ($rules_ref, $branch, $itemtype) = @_;
3190 my $discount;
3192 if (@{$rules_ref} == 1) { # only 1 applicable rule use it
3193 $discount = $rules_ref->[0]->{rentaldiscount};
3194 return (defined $discount) ? $discount : 0;
3196 # could have up to 4 does one match $branch and $itemtype
3197 my @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq $itemtype } @{$rules_ref};
3198 if (@d) {
3199 $discount = $d[0]->{rentaldiscount};
3200 return (defined $discount) ? $discount : 0;
3202 # do we have item type + all branches
3203 @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq $itemtype } @{$rules_ref};
3204 if (@d) {
3205 $discount = $d[0]->{rentaldiscount};
3206 return (defined $discount) ? $discount : 0;
3208 # do we all item types + this branch
3209 @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq q{*} } @{$rules_ref};
3210 if (@d) {
3211 $discount = $d[0]->{rentaldiscount};
3212 return (defined $discount) ? $discount : 0;
3214 # so all and all (surely we wont get here)
3215 @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq q{*} } @{$rules_ref};
3216 if (@d) {
3217 $discount = $d[0]->{rentaldiscount};
3218 return (defined $discount) ? $discount : 0;
3220 # none of the above
3221 return 0;
3224 =head2 AddIssuingCharge
3226 &AddIssuingCharge( $itemno, $borrowernumber, $charge )
3228 =cut
3230 sub AddIssuingCharge {
3231 my ( $itemnumber, $borrowernumber, $charge ) = @_;
3232 my $dbh = C4::Context->dbh;
3233 my $nextaccntno = getnextacctno( $borrowernumber );
3234 my $manager_id = 0;
3235 $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
3236 my $query ="
3237 INSERT INTO accountlines
3238 (borrowernumber, itemnumber, accountno,
3239 date, amount, description, accounttype,
3240 amountoutstanding, manager_id)
3241 VALUES (?, ?, ?,now(), ?, 'Rental', 'Rent',?,?)
3243 my $sth = $dbh->prepare($query);
3244 $sth->execute( $borrowernumber, $itemnumber, $nextaccntno, $charge, $charge, $manager_id );
3247 =head2 GetTransfers
3249 GetTransfers($itemnumber);
3251 =cut
3253 sub GetTransfers {
3254 my ($itemnumber) = @_;
3256 my $dbh = C4::Context->dbh;
3258 my $query = '
3259 SELECT datesent,
3260 frombranch,
3261 tobranch
3262 FROM branchtransfers
3263 WHERE itemnumber = ?
3264 AND datearrived IS NULL
3266 my $sth = $dbh->prepare($query);
3267 $sth->execute($itemnumber);
3268 my @row = $sth->fetchrow_array();
3269 return @row;
3272 =head2 GetTransfersFromTo
3274 @results = GetTransfersFromTo($frombranch,$tobranch);
3276 Returns the list of pending transfers between $from and $to branch
3278 =cut
3280 sub GetTransfersFromTo {
3281 my ( $frombranch, $tobranch ) = @_;
3282 return unless ( $frombranch && $tobranch );
3283 my $dbh = C4::Context->dbh;
3284 my $query = "
3285 SELECT itemnumber,datesent,frombranch
3286 FROM branchtransfers
3287 WHERE frombranch=?
3288 AND tobranch=?
3289 AND datearrived IS NULL
3291 my $sth = $dbh->prepare($query);
3292 $sth->execute( $frombranch, $tobranch );
3293 my @gettransfers;
3295 while ( my $data = $sth->fetchrow_hashref ) {
3296 push @gettransfers, $data;
3298 return (@gettransfers);
3301 =head2 DeleteTransfer
3303 &DeleteTransfer($itemnumber);
3305 =cut
3307 sub DeleteTransfer {
3308 my ($itemnumber) = @_;
3309 return unless $itemnumber;
3310 my $dbh = C4::Context->dbh;
3311 my $sth = $dbh->prepare(
3312 "DELETE FROM branchtransfers
3313 WHERE itemnumber=?
3314 AND datearrived IS NULL "
3316 return $sth->execute($itemnumber);
3319 =head2 AnonymiseIssueHistory
3321 ($rows,$err_history_not_deleted) = AnonymiseIssueHistory($date,$borrowernumber)
3323 This function write NULL instead of C<$borrowernumber> given on input arg into the table issues.
3324 if C<$borrowernumber> is not set, it will delete the issue history for all borrower older than C<$date>.
3326 If c<$borrowernumber> is set, it will delete issue history for only that borrower, regardless of their opac privacy
3327 setting (force delete).
3329 return the number of affected rows and a value that evaluates to true if an error occurred deleting the history.
3331 =cut
3333 sub AnonymiseIssueHistory {
3334 my $date = shift;
3335 my $borrowernumber = shift;
3336 my $dbh = C4::Context->dbh;
3337 my $query = "
3338 UPDATE old_issues
3339 SET borrowernumber = ?
3340 WHERE returndate < ?
3341 AND borrowernumber IS NOT NULL
3344 # The default of 0 does not work due to foreign key constraints
3345 # The anonymisation should not fail quietly if AnonymousPatron is not a valid entry
3346 # Set it to undef (NULL)
3347 my $anonymouspatron = C4::Context->preference('AnonymousPatron') || undef;
3348 my @bind_params = ($anonymouspatron, $date);
3349 if (defined $borrowernumber) {
3350 $query .= " AND borrowernumber = ?";
3351 push @bind_params, $borrowernumber;
3352 } else {
3353 $query .= " AND (SELECT privacy FROM borrowers WHERE borrowers.borrowernumber=old_issues.borrowernumber) <> 0";
3355 my $sth = $dbh->prepare($query);
3356 $sth->execute(@bind_params);
3357 my $anonymisation_err = $dbh->err;
3358 my $rows_affected = $sth->rows; ### doublecheck row count return function
3359 return ($rows_affected, $anonymisation_err);
3362 =head2 SendCirculationAlert
3364 Send out a C<check-in> or C<checkout> alert using the messaging system.
3366 B<Parameters>:
3368 =over 4
3370 =item type
3372 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
3374 =item item
3376 Hashref of information about the item being checked in or out.
3378 =item borrower
3380 Hashref of information about the borrower of the item.
3382 =item branch
3384 The branchcode from where the checkout or check-in took place.
3386 =back
3388 B<Example>:
3390 SendCirculationAlert({
3391 type => 'CHECKOUT',
3392 item => $item,
3393 borrower => $borrower,
3394 branch => $branch,
3397 =cut
3399 sub SendCirculationAlert {
3400 my ($opts) = @_;
3401 my ($type, $item, $borrower, $branch) =
3402 ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
3403 my %message_name = (
3404 CHECKIN => 'Item_Check_in',
3405 CHECKOUT => 'Item_Checkout',
3406 RENEWAL => 'Item_Checkout',
3408 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
3409 borrowernumber => $borrower->{borrowernumber},
3410 message_name => $message_name{$type},
3412 my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues';
3414 my @transports = keys %{ $borrower_preferences->{transports} };
3415 # warn "no transports" unless @transports;
3416 for (@transports) {
3417 # warn "transport: $_";
3418 my $message = C4::Message->find_last_message($borrower, $type, $_);
3419 if (!$message) {
3420 #warn "create new message";
3421 my $letter = C4::Letters::GetPreparedLetter (
3422 module => 'circulation',
3423 letter_code => $type,
3424 branchcode => $branch,
3425 message_transport_type => $_,
3426 tables => {
3427 $issues_table => $item->{itemnumber},
3428 'items' => $item->{itemnumber},
3429 'biblio' => $item->{biblionumber},
3430 'biblioitems' => $item->{biblionumber},
3431 'borrowers' => $borrower,
3432 'branches' => $branch,
3434 ) or next;
3435 C4::Message->enqueue($letter, $borrower, $_);
3436 } else {
3437 #warn "append to old message";
3438 my $letter = C4::Letters::GetPreparedLetter (
3439 module => 'circulation',
3440 letter_code => $type,
3441 branchcode => $branch,
3442 message_transport_type => $_,
3443 tables => {
3444 $issues_table => $item->{itemnumber},
3445 'items' => $item->{itemnumber},
3446 'biblio' => $item->{biblionumber},
3447 'biblioitems' => $item->{biblionumber},
3448 'borrowers' => $borrower,
3449 'branches' => $branch,
3451 ) or next;
3452 $message->append($letter);
3453 $message->update;
3457 return;
3460 =head2 updateWrongTransfer
3462 $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
3464 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
3466 =cut
3468 sub updateWrongTransfer {
3469 my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
3470 my $dbh = C4::Context->dbh;
3471 # first step validate the actual line of transfert .
3472 my $sth =
3473 $dbh->prepare(
3474 "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
3476 $sth->execute($FromLibrary,$itemNumber);
3478 # second step create a new line of branchtransfer to the right location .
3479 ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
3481 #third step changing holdingbranch of item
3482 UpdateHoldingbranch($FromLibrary,$itemNumber);
3485 =head2 UpdateHoldingbranch
3487 $items = UpdateHoldingbranch($branch,$itmenumber);
3489 Simple methode for updating hodlingbranch in items BDD line
3491 =cut
3493 sub UpdateHoldingbranch {
3494 my ( $branch,$itemnumber ) = @_;
3495 ModItem({ holdingbranch => $branch }, undef, $itemnumber);
3498 =head2 CalcDateDue
3500 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
3502 this function calculates the due date given the start date and configured circulation rules,
3503 checking against the holidays calendar as per the 'useDaysMode' syspref.
3504 C<$startdate> = DateTime object representing start date of loan period (assumed to be today)
3505 C<$itemtype> = itemtype code of item in question
3506 C<$branch> = location whose calendar to use
3507 C<$borrower> = Borrower object
3508 C<$isrenewal> = Boolean: is true if we want to calculate the date due for a renewal. Else is false.
3510 =cut
3512 sub CalcDateDue {
3513 my ( $startdate, $itemtype, $branch, $borrower, $isrenewal ) = @_;
3515 $isrenewal ||= 0;
3517 # loanlength now a href
3518 my $loanlength =
3519 GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
3521 my $length_key = ( $isrenewal and defined $loanlength->{renewalperiod} )
3522 ? qq{renewalperiod}
3523 : qq{issuelength};
3525 my $datedue;
3526 if ( $startdate ) {
3527 if (ref $startdate ne 'DateTime' ) {
3528 $datedue = dt_from_string($datedue);
3529 } else {
3530 $datedue = $startdate->clone;
3532 } else {
3533 $datedue =
3534 DateTime->now( time_zone => C4::Context->tz() )
3535 ->truncate( to => 'minute' );
3539 # calculate the datedue as normal
3540 if ( C4::Context->preference('useDaysMode') eq 'Days' )
3541 { # ignoring calendar
3542 if ( $loanlength->{lengthunit} eq 'hours' ) {
3543 $datedue->add( hours => $loanlength->{$length_key} );
3544 } else { # days
3545 $datedue->add( days => $loanlength->{$length_key} );
3546 $datedue->set_hour(23);
3547 $datedue->set_minute(59);
3549 } else {
3550 my $dur;
3551 if ($loanlength->{lengthunit} eq 'hours') {
3552 $dur = DateTime::Duration->new( hours => $loanlength->{$length_key});
3554 else { # days
3555 $dur = DateTime::Duration->new( days => $loanlength->{$length_key});
3557 my $calendar = Koha::Calendar->new( branchcode => $branch );
3558 $datedue = $calendar->addDate( $datedue, $dur, $loanlength->{lengthunit} );
3559 if ($loanlength->{lengthunit} eq 'days') {
3560 $datedue->set_hour(23);
3561 $datedue->set_minute(59);
3565 # if Hard Due Dates are used, retrieve them and apply as necessary
3566 my ( $hardduedate, $hardduedatecompare ) =
3567 GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
3568 if ($hardduedate) { # hardduedates are currently dates
3569 $hardduedate->truncate( to => 'minute' );
3570 $hardduedate->set_hour(23);
3571 $hardduedate->set_minute(59);
3572 my $cmp = DateTime->compare( $hardduedate, $datedue );
3574 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
3575 # if the calculated date is before the 'after' Hard Due Date (floor), override
3576 # if the hard due date is set to 'exactly', overrride
3577 if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
3578 $datedue = $hardduedate->clone;
3581 # in all other cases, keep the date due as it is
3585 # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3586 if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3587 my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso', 'floating');
3588 if( $expiry_dt ) { #skip empty expiry date..
3589 $expiry_dt->set( hour => 23, minute => 59);
3590 my $d1= $datedue->clone->set_time_zone('floating');
3591 if ( DateTime->compare( $d1, $expiry_dt ) == 1 ) {
3592 $datedue = $expiry_dt->clone->set_time_zone( C4::Context->tz );
3597 return $datedue;
3601 sub CheckValidBarcode{
3602 my ($barcode) = @_;
3603 my $dbh = C4::Context->dbh;
3604 my $query=qq|SELECT count(*)
3605 FROM items
3606 WHERE barcode=?
3608 my $sth = $dbh->prepare($query);
3609 $sth->execute($barcode);
3610 my $exist=$sth->fetchrow ;
3611 return $exist;
3614 =head2 IsBranchTransferAllowed
3616 $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3618 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3620 =cut
3622 sub IsBranchTransferAllowed {
3623 my ( $toBranch, $fromBranch, $code ) = @_;
3625 if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3627 my $limitType = C4::Context->preference("BranchTransferLimitsType");
3628 my $dbh = C4::Context->dbh;
3630 my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3631 $sth->execute( $toBranch, $fromBranch, $code );
3632 my $limit = $sth->fetchrow_hashref();
3634 ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3635 if ( $limit->{'limitId'} ) {
3636 return 0;
3637 } else {
3638 return 1;
3642 =head2 CreateBranchTransferLimit
3644 CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3646 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3648 =cut
3650 sub CreateBranchTransferLimit {
3651 my ( $toBranch, $fromBranch, $code ) = @_;
3652 return unless defined($toBranch) && defined($fromBranch);
3653 my $limitType = C4::Context->preference("BranchTransferLimitsType");
3655 my $dbh = C4::Context->dbh;
3657 my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3658 return $sth->execute( $code, $toBranch, $fromBranch );
3661 =head2 DeleteBranchTransferLimits
3663 my $result = DeleteBranchTransferLimits($frombranch);
3665 Deletes all the library transfer limits for one library. Returns the
3666 number of limits deleted, 0e0 if no limits were deleted, or undef if
3667 no arguments are supplied.
3669 =cut
3671 sub DeleteBranchTransferLimits {
3672 my $branch = shift;
3673 return unless defined $branch;
3674 my $dbh = C4::Context->dbh;
3675 my $sth = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3676 return $sth->execute($branch);
3679 sub ReturnLostItem{
3680 my ( $borrowernumber, $itemnum ) = @_;
3682 MarkIssueReturned( $borrowernumber, $itemnum );
3683 my $borrower = C4::Members::GetMember( 'borrowernumber'=>$borrowernumber );
3684 my $item = C4::Items::GetItem( $itemnum );
3685 my $old_note = ($item->{'paidfor'} && ($item->{'paidfor'} ne q{})) ? $item->{'paidfor'}.' / ' : q{};
3686 my @datearr = localtime(time);
3687 my $date = ( 1900 + $datearr[5] ) . "-" . ( $datearr[4] + 1 ) . "-" . $datearr[3];
3688 my $bor = "$borrower->{'firstname'} $borrower->{'surname'} $borrower->{'cardnumber'}";
3689 ModItem({ paidfor => $old_note."Paid for by $bor $date" }, undef, $itemnum);
3693 sub LostItem{
3694 my ($itemnumber, $mark_returned) = @_;
3696 my $dbh = C4::Context->dbh();
3697 my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title
3698 FROM issues
3699 JOIN items USING (itemnumber)
3700 JOIN biblio USING (biblionumber)
3701 WHERE issues.itemnumber=?");
3702 $sth->execute($itemnumber);
3703 my $issues=$sth->fetchrow_hashref();
3705 # If a borrower lost the item, add a replacement cost to the their record
3706 if ( my $borrowernumber = $issues->{borrowernumber} ){
3707 my $borrower = C4::Members::GetMemberDetails( $borrowernumber );
3709 if (C4::Context->preference('WhenLostForgiveFine')){
3710 my $fix = _FixOverduesOnReturn($borrowernumber, $itemnumber, 1, 0); # 1, 0 = exemptfine, no-dropbox
3711 defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $itemnumber...) failed!"; # zero is OK, check defined
3713 if (C4::Context->preference('WhenLostChargeReplacementFee')){
3714 C4::Accounts::chargelostitem($borrowernumber, $itemnumber, $issues->{'replacementprice'}, "Lost Item $issues->{'title'} $issues->{'barcode'}");
3715 #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3716 #warn " $issues->{'borrowernumber'} / $itemnumber ";
3719 MarkIssueReturned($borrowernumber,$itemnumber,undef,undef,$borrower->{'privacy'}) if $mark_returned;
3723 sub GetOfflineOperations {
3724 my $dbh = C4::Context->dbh;
3725 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3726 $sth->execute(C4::Context->userenv->{'branch'});
3727 my $results = $sth->fetchall_arrayref({});
3728 return $results;
3731 sub GetOfflineOperation {
3732 my $operationid = shift;
3733 return unless $operationid;
3734 my $dbh = C4::Context->dbh;
3735 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3736 $sth->execute( $operationid );
3737 return $sth->fetchrow_hashref;
3740 sub AddOfflineOperation {
3741 my ( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount ) = @_;
3742 my $dbh = C4::Context->dbh;
3743 my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber, amount) VALUES(?,?,?,?,?,?,?)");
3744 $sth->execute( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount );
3745 return "Added.";
3748 sub DeleteOfflineOperation {
3749 my $dbh = C4::Context->dbh;
3750 my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3751 $sth->execute( shift );
3752 return "Deleted.";
3755 sub ProcessOfflineOperation {
3756 my $operation = shift;
3758 my $report;
3759 if ( $operation->{action} eq 'return' ) {
3760 $report = ProcessOfflineReturn( $operation );
3761 } elsif ( $operation->{action} eq 'issue' ) {
3762 $report = ProcessOfflineIssue( $operation );
3763 } elsif ( $operation->{action} eq 'payment' ) {
3764 $report = ProcessOfflinePayment( $operation );
3767 DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3769 return $report;
3772 sub ProcessOfflineReturn {
3773 my $operation = shift;
3775 my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3777 if ( $itemnumber ) {
3778 my $issue = GetOpenIssue( $itemnumber );
3779 if ( $issue ) {
3780 MarkIssueReturned(
3781 $issue->{borrowernumber},
3782 $itemnumber,
3783 undef,
3784 $operation->{timestamp},
3786 ModItem(
3787 { renewals => 0, onloan => undef },
3788 $issue->{'biblionumber'},
3789 $itemnumber
3791 return "Success.";
3792 } else {
3793 return "Item not issued.";
3795 } else {
3796 return "Item not found.";
3800 sub ProcessOfflineIssue {
3801 my $operation = shift;
3803 my $borrower = C4::Members::GetMemberDetails( undef, $operation->{cardnumber} ); # Get borrower from operation cardnumber
3805 if ( $borrower->{borrowernumber} ) {
3806 my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3807 unless ($itemnumber) {
3808 return "Barcode not found.";
3810 my $issue = GetOpenIssue( $itemnumber );
3812 if ( $issue and ( $issue->{borrowernumber} ne $borrower->{borrowernumber} ) ) { # Item already issued to another borrower, mark it returned
3813 MarkIssueReturned(
3814 $issue->{borrowernumber},
3815 $itemnumber,
3816 undef,
3817 $operation->{timestamp},
3820 AddIssue(
3821 $borrower,
3822 $operation->{'barcode'},
3823 undef,
3825 $operation->{timestamp},
3826 undef,
3828 return "Success.";
3829 } else {
3830 return "Borrower not found.";
3834 sub ProcessOfflinePayment {
3835 my $operation = shift;
3837 my $patron = Koha::Patrons->find( { cardnumber => $operation->{cardnumber} });
3838 my $amount = $operation->{amount};
3840 Koha::Account->new( { patron_id => $patron->id } )->pay( { amount => $amount } );
3842 return "Success."
3846 =head2 TransferSlip
3848 TransferSlip($user_branch, $itemnumber, $barcode, $to_branch)
3850 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
3852 =cut
3854 sub TransferSlip {
3855 my ($branch, $itemnumber, $barcode, $to_branch) = @_;
3857 my $item = GetItem( $itemnumber, $barcode )
3858 or return;
3860 return C4::Letters::GetPreparedLetter (
3861 module => 'circulation',
3862 letter_code => 'TRANSFERSLIP',
3863 branchcode => $branch,
3864 tables => {
3865 'branches' => $to_branch,
3866 'biblio' => $item->{biblionumber},
3867 'items' => $item,
3872 =head2 CheckIfIssuedToPatron
3874 CheckIfIssuedToPatron($borrowernumber, $biblionumber)
3876 Return 1 if any record item is issued to patron, otherwise return 0
3878 =cut
3880 sub CheckIfIssuedToPatron {
3881 my ($borrowernumber, $biblionumber) = @_;
3883 my $dbh = C4::Context->dbh;
3884 my $query = q|
3885 SELECT COUNT(*) FROM issues
3886 LEFT JOIN items ON items.itemnumber = issues.itemnumber
3887 WHERE items.biblionumber = ?
3888 AND issues.borrowernumber = ?
3890 my $is_issued = $dbh->selectrow_array($query, {}, $biblionumber, $borrowernumber );
3891 return 1 if $is_issued;
3892 return;
3895 =head2 IsItemIssued
3897 IsItemIssued( $itemnumber )
3899 Return 1 if the item is on loan, otherwise return 0
3901 =cut
3903 sub IsItemIssued {
3904 my $itemnumber = shift;
3905 my $dbh = C4::Context->dbh;
3906 my $sth = $dbh->prepare(q{
3907 SELECT COUNT(*)
3908 FROM issues
3909 WHERE itemnumber = ?
3911 $sth->execute($itemnumber);
3912 return $sth->fetchrow;
3915 =head2 GetAgeRestriction
3917 my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions, $borrower);
3918 my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions);
3920 if($daysToAgeRestriction <= 0) { #Borrower is allowed to access this material, as he is older or as old as the agerestriction }
3921 if($daysToAgeRestriction > 0) { #Borrower is this many days from meeting the agerestriction }
3923 @PARAM1 the koha.biblioitems.agerestriction value, like K18, PEGI 13, ...
3924 @PARAM2 a borrower-object with koha.borrowers.dateofbirth. (OPTIONAL)
3925 @RETURNS The age restriction age in years and the days to fulfill the age restriction for the given borrower.
3926 Negative days mean the borrower has gone past the age restriction age.
3928 =cut
3930 sub GetAgeRestriction {
3931 my ($record_restrictions, $borrower) = @_;
3932 my $markers = C4::Context->preference('AgeRestrictionMarker');
3934 # Split $record_restrictions to something like FSK 16 or PEGI 6
3935 my @values = split ' ', uc($record_restrictions);
3936 return unless @values;
3938 # Search first occurrence of one of the markers
3939 my @markers = split /\|/, uc($markers);
3940 return unless @markers;
3942 my $index = 0;
3943 my $restriction_year = 0;
3944 for my $value (@values) {
3945 $index++;
3946 for my $marker (@markers) {
3947 $marker =~ s/^\s+//; #remove leading spaces
3948 $marker =~ s/\s+$//; #remove trailing spaces
3949 if ( $marker eq $value ) {
3950 if ( $index <= $#values ) {
3951 $restriction_year += $values[$index];
3953 last;
3955 elsif ( $value =~ /^\Q$marker\E(\d+)$/ ) {
3957 # Perhaps it is something like "K16" (as in Finland)
3958 $restriction_year += $1;
3959 last;
3962 last if ( $restriction_year > 0 );
3965 #Check if the borrower is age restricted for this material and for how long.
3966 if ($restriction_year && $borrower) {
3967 if ( $borrower->{'dateofbirth'} ) {
3968 my @alloweddate = split /-/, $borrower->{'dateofbirth'};
3969 $alloweddate[0] += $restriction_year;
3971 #Prevent runime eror on leap year (invalid date)
3972 if ( ( $alloweddate[1] == 2 ) && ( $alloweddate[2] == 29 ) ) {
3973 $alloweddate[2] = 28;
3976 #Get how many days the borrower has to reach the age restriction
3977 my @Today = split /-/, DateTime->today->ymd();
3978 my $daysToAgeRestriction = Date_to_Days(@alloweddate) - Date_to_Days(@Today);
3979 #Negative days means the borrower went past the age restriction age
3980 return ($restriction_year, $daysToAgeRestriction);
3984 return ($restriction_year);
3988 =head2 GetPendingOnSiteCheckouts
3990 =cut
3992 sub GetPendingOnSiteCheckouts {
3993 my $dbh = C4::Context->dbh;
3994 return $dbh->selectall_arrayref(q|
3995 SELECT
3996 items.barcode,
3997 items.biblionumber,
3998 items.itemnumber,
3999 items.itemnotes,
4000 items.itemcallnumber,
4001 items.location,
4002 issues.date_due,
4003 issues.branchcode,
4004 issues.date_due < NOW() AS is_overdue,
4005 biblio.author,
4006 biblio.title,
4007 borrowers.firstname,
4008 borrowers.surname,
4009 borrowers.cardnumber,
4010 borrowers.borrowernumber
4011 FROM items
4012 LEFT JOIN issues ON items.itemnumber = issues.itemnumber
4013 LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
4014 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
4015 WHERE issues.onsite_checkout = 1
4016 |, { Slice => {} } );
4019 sub GetTopIssues {
4020 my ($params) = @_;
4022 my ($count, $branch, $itemtype, $ccode, $newness)
4023 = @$params{qw(count branch itemtype ccode newness)};
4025 my $dbh = C4::Context->dbh;
4026 my $query = q{
4027 SELECT b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4028 bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4029 i.ccode, SUM(i.issues) AS count
4030 FROM biblio b
4031 LEFT JOIN items i ON (i.biblionumber = b.biblionumber)
4032 LEFT JOIN biblioitems bi ON (bi.biblionumber = b.biblionumber)
4035 my (@where_strs, @where_args);
4037 if ($branch) {
4038 push @where_strs, 'i.homebranch = ?';
4039 push @where_args, $branch;
4041 if ($itemtype) {
4042 if (C4::Context->preference('item-level_itypes')){
4043 push @where_strs, 'i.itype = ?';
4044 push @where_args, $itemtype;
4045 } else {
4046 push @where_strs, 'bi.itemtype = ?';
4047 push @where_args, $itemtype;
4050 if ($ccode) {
4051 push @where_strs, 'i.ccode = ?';
4052 push @where_args, $ccode;
4054 if ($newness) {
4055 push @where_strs, 'TO_DAYS(NOW()) - TO_DAYS(b.datecreated) <= ?';
4056 push @where_args, $newness;
4059 if (@where_strs) {
4060 $query .= 'WHERE ' . join(' AND ', @where_strs);
4063 $query .= q{
4064 GROUP BY b.biblionumber
4065 HAVING count > 0
4066 ORDER BY count DESC
4069 $count = int($count);
4070 if ($count > 0) {
4071 $query .= "LIMIT $count";
4074 my $rows = $dbh->selectall_arrayref($query, { Slice => {} }, @where_args);
4076 return @$rows;
4079 sub _CalculateAndUpdateFine {
4080 my ($params) = @_;
4082 my $borrower = $params->{borrower};
4083 my $item = $params->{item};
4084 my $issue = $params->{issue};
4085 my $return_date = $params->{return_date};
4087 unless ($borrower) { carp "No borrower passed in!" && return; }
4088 unless ($item) { carp "No item passed in!" && return; }
4089 unless ($issue) { carp "No issue passed in!" && return; }
4091 my $datedue = $issue->{date_due};
4093 # we only need to calculate and change the fines if we want to do that on return
4094 # Should be on for hourly loans
4095 my $control = C4::Context->preference('CircControl');
4096 my $control_branchcode =
4097 ( $control eq 'ItemHomeLibrary' ) ? $item->{homebranch}
4098 : ( $control eq 'PatronLibrary' ) ? $borrower->{branchcode}
4099 : $issue->{branchcode};
4101 my $date_returned = $return_date ? dt_from_string($return_date) : dt_from_string();
4103 my ( $amount, $type, $unitcounttotal ) =
4104 C4::Overdues::CalcFine( $item, $borrower->{categorycode}, $control_branchcode, $datedue, $date_returned );
4106 $type ||= q{};
4108 if ( C4::Context->preference('finesMode') eq 'production' ) {
4109 if ( $amount > 0 ) {
4110 C4::Overdues::UpdateFine({
4111 issue_id => $issue->{issue_id},
4112 itemnumber => $issue->{itemnumber},
4113 borrowernumber => $issue->{borrowernumber},
4114 amount => $amount,
4115 type => $type,
4116 due => output_pref($datedue),
4119 elsif ($return_date) {
4121 # Backdated returns may have fines that shouldn't exist,
4122 # so in this case, we need to drop those fines to 0
4124 C4::Overdues::UpdateFine({
4125 issue_id => $issue->{issue_id},
4126 itemnumber => $issue->{itemnumber},
4127 borrowernumber => $issue->{borrowernumber},
4128 amount => 0,
4129 type => $type,
4130 due => output_pref($datedue),
4138 __END__
4140 =head1 AUTHOR
4142 Koha Development Team <http://koha-community.org/>
4144 =cut