Bug 16843: Help for EDIFACT messages
[koha.git] / C4 / Circulation.pm
blob19f9a9a1a17a04bae4928d5d7b4068180d50ad5e
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
40 GetKohaAuthorisedValueLib
42 use C4::Overdues qw(CalcFine UpdateFine get_chargeable_units);
43 use C4::RotatingCollections qw(GetCollectionItemBranches);
44 use Algorithm::CheckDigits;
46 use Data::Dumper;
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 $cat_borrower = $borrower->{'categorycode'};
391 my $dbh = C4::Context->dbh;
392 my $branch;
393 # Get which branchcode we need
394 $branch = _GetCircControlBranch($item,$borrower);
395 my $type = (C4::Context->preference('item-level_itypes'))
396 ? $item->{'itype'} # item-level
397 : $item->{'itemtype'}; # biblio-level
399 # given branch, patron category, and item type, determine
400 # applicable issuing rule
401 my $issuing_rule = GetIssuingRule($cat_borrower, $type, $branch);
403 # if a rule is found and has a loan limit set, count
404 # how many loans the patron already has that meet that
405 # rule
406 if (defined($issuing_rule) and defined($issuing_rule->{'maxissueqty'})) {
407 my @bind_params;
408 my $count_query = q|
409 SELECT COUNT(*) AS total, COALESCE(SUM(onsite_checkout), 0) AS onsite_checkouts
410 FROM issues
411 JOIN items USING (itemnumber)
414 my $rule_itemtype = $issuing_rule->{itemtype};
415 if ($rule_itemtype eq "*") {
416 # matching rule has the default item type, so count only
417 # those existing loans that don't fall under a more
418 # specific rule
419 if (C4::Context->preference('item-level_itypes')) {
420 $count_query .= " WHERE items.itype NOT IN (
421 SELECT itemtype FROM issuingrules
422 WHERE branchcode = ?
423 AND (categorycode = ? OR categorycode = ?)
424 AND itemtype <> '*'
425 ) ";
426 } else {
427 $count_query .= " JOIN biblioitems USING (biblionumber)
428 WHERE biblioitems.itemtype NOT IN (
429 SELECT itemtype FROM issuingrules
430 WHERE branchcode = ?
431 AND (categorycode = ? OR categorycode = ?)
432 AND itemtype <> '*'
433 ) ";
435 push @bind_params, $issuing_rule->{branchcode};
436 push @bind_params, $issuing_rule->{categorycode};
437 push @bind_params, $cat_borrower;
438 } else {
439 # rule has specific item type, so count loans of that
440 # specific item type
441 if (C4::Context->preference('item-level_itypes')) {
442 $count_query .= " WHERE items.itype = ? ";
443 } else {
444 $count_query .= " JOIN biblioitems USING (biblionumber)
445 WHERE biblioitems.itemtype= ? ";
447 push @bind_params, $type;
450 $count_query .= " AND borrowernumber = ? ";
451 push @bind_params, $borrower->{'borrowernumber'};
452 my $rule_branch = $issuing_rule->{branchcode};
453 if ($rule_branch ne "*") {
454 if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
455 $count_query .= " AND issues.branchcode = ? ";
456 push @bind_params, $branch;
457 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
458 ; # if branch is the patron's home branch, then count all loans by patron
459 } else {
460 $count_query .= " AND items.homebranch = ? ";
461 push @bind_params, $branch;
465 my ( $checkout_count, $onsite_checkout_count ) = $dbh->selectrow_array( $count_query, {}, @bind_params );
467 my $max_checkouts_allowed = $issuing_rule->{maxissueqty};
468 my $max_onsite_checkouts_allowed = $issuing_rule->{maxonsiteissueqty};
470 if ( $onsite_checkout ) {
471 if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed ) {
472 return {
473 reason => 'TOO_MANY_ONSITE_CHECKOUTS',
474 count => $onsite_checkout_count,
475 max_allowed => $max_onsite_checkouts_allowed,
479 if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) {
480 if ( $checkout_count >= $max_checkouts_allowed ) {
481 return {
482 reason => 'TOO_MANY_CHECKOUTS',
483 count => $checkout_count,
484 max_allowed => $max_checkouts_allowed,
487 } elsif ( not $onsite_checkout ) {
488 if ( $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed ) {
489 return {
490 reason => 'TOO_MANY_CHECKOUTS',
491 count => $checkout_count - $onsite_checkout_count,
492 max_allowed => $max_checkouts_allowed,
498 # Now count total loans against the limit for the branch
499 my $branch_borrower_circ_rule = GetBranchBorrowerCircRule($branch, $cat_borrower);
500 if (defined($branch_borrower_circ_rule->{maxissueqty})) {
501 my @bind_params = ();
502 my $branch_count_query = q|
503 SELECT COUNT(*) AS total, COALESCE(SUM(onsite_checkout), 0) AS onsite_checkouts
504 FROM issues
505 JOIN items USING (itemnumber)
506 WHERE borrowernumber = ?
508 push @bind_params, $borrower->{borrowernumber};
510 if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
511 $branch_count_query .= " AND issues.branchcode = ? ";
512 push @bind_params, $branch;
513 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
514 ; # if branch is the patron's home branch, then count all loans by patron
515 } else {
516 $branch_count_query .= " AND items.homebranch = ? ";
517 push @bind_params, $branch;
519 my ( $checkout_count, $onsite_checkout_count ) = $dbh->selectrow_array( $branch_count_query, {}, @bind_params );
520 my $max_checkouts_allowed = $branch_borrower_circ_rule->{maxissueqty};
521 my $max_onsite_checkouts_allowed = $branch_borrower_circ_rule->{maxonsiteissueqty};
523 if ( $onsite_checkout ) {
524 if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed ) {
525 return {
526 reason => 'TOO_MANY_ONSITE_CHECKOUTS',
527 count => $onsite_checkout_count,
528 max_allowed => $max_onsite_checkouts_allowed,
532 if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) {
533 if ( $checkout_count >= $max_checkouts_allowed ) {
534 return {
535 reason => 'TOO_MANY_CHECKOUTS',
536 count => $checkout_count,
537 max_allowed => $max_checkouts_allowed,
540 } elsif ( not $onsite_checkout ) {
541 if ( $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed ) {
542 return {
543 reason => 'TOO_MANY_CHECKOUTS',
544 count => $checkout_count - $onsite_checkout_count,
545 max_allowed => $max_checkouts_allowed,
551 # OK, the patron can issue !!!
552 return;
555 =head2 CanBookBeIssued
557 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $borrower,
558 $barcode, $duedate, $inprocess, $ignore_reserves, $params );
560 Check if a book can be issued.
562 C<$issuingimpossible> and C<$needsconfirmation> are some hashref.
564 =over 4
566 =item C<$borrower> hash with borrower informations (from GetMember or GetMemberDetails)
568 =item C<$barcode> is the bar code of the book being issued.
570 =item C<$duedates> is a DateTime object.
572 =item C<$inprocess> boolean switch
574 =item C<$ignore_reserves> boolean switch
576 =item C<$params> Hashref of additional parameters
578 Available keys:
579 override_high_holds - Ignore high holds
580 onsite_checkout - Checkout is an onsite checkout that will not leave the library
582 =back
584 Returns :
586 =over 4
588 =item C<$issuingimpossible> a reference to a hash. It contains reasons why issuing is impossible.
589 Possible values are :
591 =back
593 =head3 INVALID_DATE
595 sticky due date is invalid
597 =head3 GNA
599 borrower gone with no address
601 =head3 CARD_LOST
603 borrower declared it's card lost
605 =head3 DEBARRED
607 borrower debarred
609 =head3 UNKNOWN_BARCODE
611 barcode unknown
613 =head3 NOT_FOR_LOAN
615 item is not for loan
617 =head3 WTHDRAWN
619 item withdrawn.
621 =head3 RESTRICTED
623 item is restricted (set by ??)
625 C<$needsconfirmation> a reference to a hash. It contains reasons why the loan
626 could be prevented, but ones that can be overriden by the operator.
628 Possible values are :
630 =head3 DEBT
632 borrower has debts.
634 =head3 RENEW_ISSUE
636 renewing, not issuing
638 =head3 ISSUED_TO_ANOTHER
640 issued to someone else.
642 =head3 RESERVED
644 reserved for someone else.
646 =head3 INVALID_DATE
648 sticky due date is invalid or due date in the past
650 =head3 TOO_MANY
652 if the borrower borrows to much things
654 =cut
656 sub CanBookBeIssued {
657 my ( $borrower, $barcode, $duedate, $inprocess, $ignore_reserves, $params ) = @_;
658 my %needsconfirmation; # filled with problems that needs confirmations
659 my %issuingimpossible; # filled with problems that causes the issue to be IMPOSSIBLE
660 my %alerts; # filled with messages that shouldn't stop issuing, but the librarian should be aware of.
662 my $onsite_checkout = $params->{onsite_checkout} || 0;
663 my $override_high_holds = $params->{override_high_holds} || 0;
665 my $item = GetItem(GetItemnumberFromBarcode( $barcode ));
666 my $issue = GetItemIssue($item->{itemnumber});
667 my $biblioitem = GetBiblioItemData($item->{biblioitemnumber});
668 $item->{'itemtype'}=$item->{'itype'};
669 my $dbh = C4::Context->dbh;
671 # MANDATORY CHECKS - unless item exists, nothing else matters
672 unless ( $item->{barcode} ) {
673 $issuingimpossible{UNKNOWN_BARCODE} = 1;
675 return ( \%issuingimpossible, \%needsconfirmation ) if %issuingimpossible;
678 # DUE DATE is OK ? -- should already have checked.
680 if ($duedate && ref $duedate ne 'DateTime') {
681 $duedate = dt_from_string($duedate);
683 my $now = DateTime->now( time_zone => C4::Context->tz() );
684 unless ( $duedate ) {
685 my $issuedate = $now->clone();
687 my $branch = _GetCircControlBranch($item,$borrower);
688 my $itype = ( C4::Context->preference('item-level_itypes') ) ? $item->{'itype'} : $biblioitem->{'itemtype'};
689 $duedate = CalcDateDue( $issuedate, $itype, $branch, $borrower );
691 # Offline circ calls AddIssue directly, doesn't run through here
692 # So issuingimpossible should be ok.
694 if ($duedate) {
695 my $today = $now->clone();
696 $today->truncate( to => 'minute');
697 if (DateTime->compare($duedate,$today) == -1 ) { # duedate cannot be before now
698 $needsconfirmation{INVALID_DATE} = output_pref($duedate);
700 } else {
701 $issuingimpossible{INVALID_DATE} = output_pref($duedate);
705 # BORROWER STATUS
707 if ( $borrower->{'category_type'} eq 'X' && ( $item->{barcode} )) {
708 # stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1 .
709 &UpdateStats({
710 branch => C4::Context->userenv->{'branch'},
711 type => 'localuse',
712 itemnumber => $item->{'itemnumber'},
713 itemtype => $item->{'itemtype'},
714 borrowernumber => $borrower->{'borrowernumber'},
715 ccode => $item->{'ccode'}}
717 ModDateLastSeen( $item->{'itemnumber'} );
718 return( { STATS => 1 }, {});
720 if ( ref $borrower->{flags} ) {
721 if ( $borrower->{flags}->{GNA} ) {
722 $issuingimpossible{GNA} = 1;
724 if ( $borrower->{flags}->{'LOST'} ) {
725 $issuingimpossible{CARD_LOST} = 1;
727 if ( $borrower->{flags}->{'DBARRED'} ) {
728 $issuingimpossible{DEBARRED} = 1;
731 if ( !defined $borrower->{dateexpiry} || $borrower->{'dateexpiry'} eq '0000-00-00') {
732 $issuingimpossible{EXPIRED} = 1;
733 } else {
734 my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'sql', 'floating' );
735 $expiry_dt->truncate( to => 'day');
736 my $today = $now->clone()->truncate(to => 'day');
737 $today->set_time_zone( 'floating' );
738 if ( DateTime->compare($today, $expiry_dt) == 1 ) {
739 $issuingimpossible{EXPIRED} = 1;
744 # BORROWER STATUS
747 # DEBTS
748 my ($balance, $non_issue_charges, $other_charges) =
749 C4::Members::GetMemberAccountBalance( $borrower->{'borrowernumber'} );
751 my $amountlimit = C4::Context->preference("noissuescharge");
752 my $allowfineoverride = C4::Context->preference("AllowFineOverride");
753 my $allfinesneedoverride = C4::Context->preference("AllFinesNeedOverride");
755 # Check the debt of this patrons guarantees
756 my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
757 $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
758 if ( defined $no_issues_charge_guarantees ) {
759 my $p = Koha::Patrons->find( $borrower->{borrowernumber} );
760 my @guarantees = $p->guarantees();
761 my $guarantees_non_issues_charges;
762 foreach my $g ( @guarantees ) {
763 my ( $b, $n, $o ) = C4::Members::GetMemberAccountBalance( $g->id );
764 $guarantees_non_issues_charges += $n;
767 if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && !$allowfineoverride) {
768 $issuingimpossible{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
769 } elsif ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && $allowfineoverride) {
770 $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
771 } elsif ( $allfinesneedoverride && $guarantees_non_issues_charges > 0 && $guarantees_non_issues_charges <= $no_issues_charge_guarantees && !$inprocess ) {
772 $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
776 if ( C4::Context->preference("IssuingInProcess") ) {
777 if ( $non_issue_charges > $amountlimit && !$inprocess && !$allowfineoverride) {
778 $issuingimpossible{DEBT} = sprintf( "%.2f", $non_issue_charges );
779 } elsif ( $non_issue_charges > $amountlimit && !$inprocess && $allowfineoverride) {
780 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
781 } elsif ( $allfinesneedoverride && $non_issue_charges > 0 && $non_issue_charges <= $amountlimit && !$inprocess ) {
782 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
785 else {
786 if ( $non_issue_charges > $amountlimit && $allowfineoverride ) {
787 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
788 } elsif ( $non_issue_charges > $amountlimit && !$allowfineoverride) {
789 $issuingimpossible{DEBT} = sprintf( "%.2f", $non_issue_charges );
790 } elsif ( $non_issue_charges > 0 && $allfinesneedoverride ) {
791 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
795 if ($balance > 0 && $other_charges > 0) {
796 $alerts{OTHER_CHARGES} = sprintf( "%.2f", $other_charges );
799 my ($blocktype, $count) = C4::Members::IsMemberBlocked($borrower->{'borrowernumber'});
800 if ($blocktype == -1) {
801 ## patron has outstanding overdue loans
802 if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){
803 $issuingimpossible{USERBLOCKEDOVERDUE} = $count;
805 elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){
806 $needsconfirmation{USERBLOCKEDOVERDUE} = $count;
808 } elsif($blocktype == 1) {
809 # patron has accrued fine days or has a restriction. $count is a date
810 if ($count eq '9999-12-31') {
811 $issuingimpossible{USERBLOCKEDNOENDDATE} = $count;
813 else {
814 $issuingimpossible{USERBLOCKEDWITHENDDATE} = $count;
819 # JB34 CHECKS IF BORROWERS DON'T HAVE ISSUE TOO MANY BOOKS
821 my $toomany = TooMany( $borrower, $item->{biblionumber}, $item, { onsite_checkout => $onsite_checkout } );
822 # if TooMany max_allowed returns 0 the user doesn't have permission to check out this book
823 if ( $toomany ) {
824 if ( $toomany->{max_allowed} == 0 ) {
825 $needsconfirmation{PATRON_CANT} = 1;
827 if ( C4::Context->preference("AllowTooManyOverride") ) {
828 $needsconfirmation{TOO_MANY} = $toomany->{reason};
829 $needsconfirmation{current_loan_count} = $toomany->{count};
830 $needsconfirmation{max_loans_allowed} = $toomany->{max_allowed};
831 } else {
832 $issuingimpossible{TOO_MANY} = $toomany->{reason};
833 $issuingimpossible{current_loan_count} = $toomany->{count};
834 $issuingimpossible{max_loans_allowed} = $toomany->{max_allowed};
839 # CHECKPREVCHECKOUT: CHECK IF ITEM HAS EVER BEEN LENT TO PATRON
841 my $patron = Koha::Patrons->find($borrower->{borrowernumber});
842 my $wants_check = $patron->wants_check_for_previous_checkout;
843 $needsconfirmation{PREVISSUE} = 1
844 if ($wants_check and $patron->do_check_for_previous_checkout($item));
847 # ITEM CHECKING
849 if ( $item->{'notforloan'} )
851 if(!C4::Context->preference("AllowNotForLoanOverride")){
852 $issuingimpossible{NOT_FOR_LOAN} = 1;
853 $issuingimpossible{item_notforloan} = $item->{'notforloan'};
854 }else{
855 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
856 $needsconfirmation{item_notforloan} = $item->{'notforloan'};
859 else {
860 # we have to check itemtypes.notforloan also
861 if (C4::Context->preference('item-level_itypes')){
862 # this should probably be a subroutine
863 my $sth = $dbh->prepare("SELECT notforloan FROM itemtypes WHERE itemtype = ?");
864 $sth->execute($item->{'itemtype'});
865 my $notforloan=$sth->fetchrow_hashref();
866 if ($notforloan->{'notforloan'}) {
867 if (!C4::Context->preference("AllowNotForLoanOverride")) {
868 $issuingimpossible{NOT_FOR_LOAN} = 1;
869 $issuingimpossible{itemtype_notforloan} = $item->{'itype'};
870 } else {
871 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
872 $needsconfirmation{itemtype_notforloan} = $item->{'itype'};
876 elsif ($biblioitem->{'notforloan'} == 1){
877 if (!C4::Context->preference("AllowNotForLoanOverride")) {
878 $issuingimpossible{NOT_FOR_LOAN} = 1;
879 $issuingimpossible{itemtype_notforloan} = $biblioitem->{'itemtype'};
880 } else {
881 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
882 $needsconfirmation{itemtype_notforloan} = $biblioitem->{'itemtype'};
886 if ( $item->{'withdrawn'} && $item->{'withdrawn'} > 0 )
888 $issuingimpossible{WTHDRAWN} = 1;
890 if ( $item->{'restricted'}
891 && $item->{'restricted'} == 1 )
893 $issuingimpossible{RESTRICTED} = 1;
895 if ( $item->{'itemlost'} && C4::Context->preference("IssueLostItem") ne 'nothing' ) {
896 my $code = GetAuthorisedValueByCode( 'LOST', $item->{'itemlost'} );
897 $needsconfirmation{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'confirm' );
898 $alerts{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'alert' );
900 if ( C4::Context->preference("IndependentBranches") ) {
901 my $userenv = C4::Context->userenv;
902 unless ( C4::Context->IsSuperLibrarian() ) {
903 if ( $item->{C4::Context->preference("HomeOrHoldingBranch")} ne $userenv->{branch} ){
904 $issuingimpossible{ITEMNOTSAMEBRANCH} = 1;
905 $issuingimpossible{'itemhomebranch'} = $item->{C4::Context->preference("HomeOrHoldingBranch")};
907 $needsconfirmation{BORRNOTSAMEBRANCH} = $borrower->{'branchcode'}
908 if ( $borrower->{'branchcode'} ne $userenv->{branch} );
912 # CHECK IF THERE IS RENTAL CHARGES. RENTAL MUST BE CONFIRMED BY THE BORROWER
914 my $rentalConfirmation = C4::Context->preference("RentalFeesCheckoutConfirmation");
916 if ( $rentalConfirmation ){
917 my ($rentalCharge) = GetIssuingCharges( $item->{'itemnumber'}, $borrower->{'borrowernumber'} );
918 if ( $rentalCharge > 0 ){
919 $rentalCharge = sprintf("%.02f", $rentalCharge);
920 $needsconfirmation{RENTALCHARGE} = $rentalCharge;
925 # CHECK IF BOOK ALREADY ISSUED TO THIS BORROWER
927 if ( $issue->{borrowernumber} && $issue->{borrowernumber} eq $borrower->{'borrowernumber'} ){
929 # Already issued to current borrower. Ask whether the loan should
930 # be renewed.
931 my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed(
932 $borrower->{'borrowernumber'},
933 $item->{'itemnumber'}
935 if ( $CanBookBeRenewed == 0 ) { # no more renewals allowed
936 if ( $renewerror eq 'onsite_checkout' ) {
937 $issuingimpossible{NO_RENEWAL_FOR_ONSITE_CHECKOUTS} = 1;
939 else {
940 $issuingimpossible{NO_MORE_RENEWALS} = 1;
943 else {
944 $needsconfirmation{RENEW_ISSUE} = 1;
947 elsif ($issue->{borrowernumber}) {
949 # issued to someone else
950 my $currborinfo = C4::Members::GetMember( borrowernumber => $issue->{borrowernumber} );
953 my ( $can_be_returned, $message ) = CanBookBeReturned( $item, C4::Context->userenv->{branch} );
955 unless ( $can_be_returned ) {
956 $issuingimpossible{RETURN_IMPOSSIBLE} = 1;
957 $issuingimpossible{branch_to_return} = $message;
958 } else {
959 $needsconfirmation{ISSUED_TO_ANOTHER} = 1;
960 $needsconfirmation{issued_firstname} = $currborinfo->{'firstname'};
961 $needsconfirmation{issued_surname} = $currborinfo->{'surname'};
962 $needsconfirmation{issued_cardnumber} = $currborinfo->{'cardnumber'};
963 $needsconfirmation{issued_borrowernumber} = $currborinfo->{'borrowernumber'};
967 unless ( $ignore_reserves ) {
968 # See if the item is on reserve.
969 my ( $restype, $res ) = C4::Reserves::CheckReserves( $item->{'itemnumber'} );
970 if ($restype) {
971 my $resbor = $res->{'borrowernumber'};
972 if ( $resbor ne $borrower->{'borrowernumber'} ) {
973 my ( $resborrower ) = C4::Members::GetMember( borrowernumber => $resbor );
974 if ( $restype eq "Waiting" )
976 # The item is on reserve and waiting, but has been
977 # reserved by some other patron.
978 $needsconfirmation{RESERVE_WAITING} = 1;
979 $needsconfirmation{'resfirstname'} = $resborrower->{'firstname'};
980 $needsconfirmation{'ressurname'} = $resborrower->{'surname'};
981 $needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'};
982 $needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'};
983 $needsconfirmation{'resbranchcode'} = $res->{branchcode};
984 $needsconfirmation{'reswaitingdate'} = $res->{'waitingdate'};
986 elsif ( $restype eq "Reserved" ) {
987 # The item is on reserve for someone else.
988 $needsconfirmation{RESERVED} = 1;
989 $needsconfirmation{'resfirstname'} = $resborrower->{'firstname'};
990 $needsconfirmation{'ressurname'} = $resborrower->{'surname'};
991 $needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'};
992 $needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'};
993 $needsconfirmation{'resbranchcode'} = $res->{branchcode};
994 $needsconfirmation{'resreservedate'} = $res->{'reservedate'};
1000 ## CHECK AGE RESTRICTION
1001 my $agerestriction = $biblioitem->{'agerestriction'};
1002 my ($restriction_age, $daysToAgeRestriction) = GetAgeRestriction( $agerestriction, $borrower );
1003 if ( $daysToAgeRestriction && $daysToAgeRestriction > 0 ) {
1004 if ( C4::Context->preference('AgeRestrictionOverride') ) {
1005 $needsconfirmation{AGE_RESTRICTION} = "$agerestriction";
1007 else {
1008 $issuingimpossible{AGE_RESTRICTION} = "$agerestriction";
1012 ## check for high holds decreasing loan period
1013 if ( C4::Context->preference('decreaseLoanHighHolds') ) {
1014 my $check = checkHighHolds( $item, $borrower );
1016 if ( $check->{exceeded} ) {
1017 if ($override_high_holds) {
1018 $alerts{HIGHHOLDS} = {
1019 num_holds => $check->{outstanding},
1020 duration => $check->{duration},
1021 returndate => output_pref( $check->{due_date} ),
1024 else {
1025 $needsconfirmation{HIGHHOLDS} = {
1026 num_holds => $check->{outstanding},
1027 duration => $check->{duration},
1028 returndate => output_pref( $check->{due_date} ),
1034 if (
1035 !C4::Context->preference('AllowMultipleIssuesOnABiblio') &&
1036 # don't do the multiple loans per bib check if we've
1037 # already determined that we've got a loan on the same item
1038 !$issuingimpossible{NO_MORE_RENEWALS} &&
1039 !$needsconfirmation{RENEW_ISSUE}
1041 # Check if borrower has already issued an item from the same biblio
1042 # Only if it's not a subscription
1043 my $biblionumber = $item->{biblionumber};
1044 require C4::Serials;
1045 my $is_a_subscription = C4::Serials::CountSubscriptionFromBiblionumber($biblionumber);
1046 unless ($is_a_subscription) {
1047 my $issues = GetIssues( {
1048 borrowernumber => $borrower->{borrowernumber},
1049 biblionumber => $biblionumber,
1050 } );
1051 my @issues = $issues ? @$issues : ();
1052 # if we get here, we don't already have a loan on this item,
1053 # so if there are any loans on this bib, ask for confirmation
1054 if (scalar @issues > 0) {
1055 $needsconfirmation{BIBLIO_ALREADY_ISSUED} = 1;
1060 return ( \%issuingimpossible, \%needsconfirmation, \%alerts );
1063 =head2 CanBookBeReturned
1065 ($returnallowed, $message) = CanBookBeReturned($item, $branch)
1067 Check whether the item can be returned to the provided branch
1069 =over 4
1071 =item C<$item> is a hash of item information as returned from GetItem
1073 =item C<$branch> is the branchcode where the return is taking place
1075 =back
1077 Returns:
1079 =over 4
1081 =item C<$returnallowed> is 0 or 1, corresponding to whether the return is allowed (1) or not (0)
1083 =item C<$message> is the branchcode where the item SHOULD be returned, if the return is not allowed
1085 =back
1087 =cut
1089 sub CanBookBeReturned {
1090 my ($item, $branch) = @_;
1091 my $allowreturntobranch = C4::Context->preference("AllowReturnToBranch") || 'anywhere';
1093 # assume return is allowed to start
1094 my $allowed = 1;
1095 my $message;
1097 # identify all cases where return is forbidden
1098 if ($allowreturntobranch eq 'homebranch' && $branch ne $item->{'homebranch'}) {
1099 $allowed = 0;
1100 $message = $item->{'homebranch'};
1101 } elsif ($allowreturntobranch eq 'holdingbranch' && $branch ne $item->{'holdingbranch'}) {
1102 $allowed = 0;
1103 $message = $item->{'holdingbranch'};
1104 } elsif ($allowreturntobranch eq 'homeorholdingbranch' && $branch ne $item->{'homebranch'} && $branch ne $item->{'holdingbranch'}) {
1105 $allowed = 0;
1106 $message = $item->{'homebranch'}; # FIXME: choice of homebranch is arbitrary
1109 return ($allowed, $message);
1112 =head2 CheckHighHolds
1114 used when syspref decreaseLoanHighHolds is active. Returns 1 or 0 to define whether the minimum value held in
1115 decreaseLoanHighHoldsValue is exceeded, the total number of outstanding holds, the number of days the loan
1116 has been decreased to (held in syspref decreaseLoanHighHoldsValue), and the new due date
1118 =cut
1120 sub checkHighHolds {
1121 my ( $item, $borrower ) = @_;
1122 my $biblio = GetBiblioFromItemNumber( $item->{itemnumber} );
1123 my $branch = _GetCircControlBranch( $item, $borrower );
1125 my $return_data = {
1126 exceeded => 0,
1127 outstanding => 0,
1128 duration => 0,
1129 due_date => undef,
1132 my $holds = Koha::Holds->search( { biblionumber => $item->{'biblionumber'} } );
1134 if ( $holds->count() ) {
1135 $return_data->{outstanding} = $holds->count();
1137 my $decreaseLoanHighHoldsControl = C4::Context->preference('decreaseLoanHighHoldsControl');
1138 my $decreaseLoanHighHoldsValue = C4::Context->preference('decreaseLoanHighHoldsValue');
1139 my $decreaseLoanHighHoldsIgnoreStatuses = C4::Context->preference('decreaseLoanHighHoldsIgnoreStatuses');
1141 my @decreaseLoanHighHoldsIgnoreStatuses = split( /,/, $decreaseLoanHighHoldsIgnoreStatuses );
1143 if ( $decreaseLoanHighHoldsControl eq 'static' ) {
1145 # static means just more than a given number of holds on the record
1147 # If the number of holds is less than the threshold, we can stop here
1148 if ( $holds->count() < $decreaseLoanHighHoldsValue ) {
1149 return $return_data;
1152 elsif ( $decreaseLoanHighHoldsControl eq 'dynamic' ) {
1154 # dynamic means X more than the number of holdable items on the record
1156 # let's get the items
1157 my @items = $holds->next()->biblio()->items();
1159 # Remove any items with status defined to be ignored even if the would not make item unholdable
1160 foreach my $status (@decreaseLoanHighHoldsIgnoreStatuses) {
1161 @items = grep { !$_->$status } @items;
1164 # Remove any items that are not holdable for this patron
1165 @items = grep { CanItemBeReserved( $borrower->{borrowernumber}, $_->itemnumber ) eq 'OK' } @items;
1167 my $items_count = scalar @items;
1169 my $threshold = $items_count + $decreaseLoanHighHoldsValue;
1171 # If the number of holds is less than the count of items we have
1172 # plus the number of holds allowed above that count, we can stop here
1173 if ( $holds->count() <= $threshold ) {
1174 return $return_data;
1178 my $issuedate = DateTime->now( time_zone => C4::Context->tz() );
1180 my $calendar = Koha::Calendar->new( branchcode => $branch );
1182 my $itype =
1183 ( C4::Context->preference('item-level_itypes') )
1184 ? $biblio->{'itype'}
1185 : $biblio->{'itemtype'};
1187 my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branch, $borrower );
1189 my $decreaseLoanHighHoldsDuration = C4::Context->preference('decreaseLoanHighHoldsDuration');
1191 my $reduced_datedue = $calendar->addDate( $issuedate, $decreaseLoanHighHoldsDuration );
1193 if ( DateTime->compare( $reduced_datedue, $orig_due ) == -1 ) {
1194 $return_data->{exceeded} = 1;
1195 $return_data->{duration} = $decreaseLoanHighHoldsDuration;
1196 $return_data->{due_date} = $reduced_datedue;
1200 return $return_data;
1203 =head2 AddIssue
1205 &AddIssue($borrower, $barcode, [$datedue], [$cancelreserve], [$issuedate])
1207 Issue a book. Does no check, they are done in CanBookBeIssued. If we reach this sub, it means the user confirmed if needed.
1209 =over 4
1211 =item C<$borrower> is a hash with borrower informations (from GetMember or GetMemberDetails).
1213 =item C<$barcode> is the barcode of the item being issued.
1215 =item C<$datedue> is a DateTime object for the max date of return, i.e. the date due (optional).
1216 Calculated if empty.
1218 =item C<$cancelreserve> is 1 to override and cancel any pending reserves for the item (optional).
1220 =item C<$issuedate> is the date to issue the item in iso (YYYY-MM-DD) format (optional).
1221 Defaults to today. Unlike C<$datedue>, NOT a DateTime object, unfortunately.
1223 AddIssue does the following things :
1225 - step 01: check that there is a borrowernumber & a barcode provided
1226 - check for RENEWAL (book issued & being issued to the same patron)
1227 - renewal YES = Calculate Charge & renew
1228 - renewal NO =
1229 * BOOK ACTUALLY ISSUED ? do a return if book is actually issued (but to someone else)
1230 * RESERVE PLACED ?
1231 - fill reserve if reserve to this patron
1232 - cancel reserve or not, otherwise
1233 * TRANSFERT PENDING ?
1234 - complete the transfert
1235 * ISSUE THE BOOK
1237 =back
1239 =cut
1241 sub AddIssue {
1242 my ( $borrower, $barcode, $datedue, $cancelreserve, $issuedate, $sipmode, $params ) = @_;
1244 my $onsite_checkout = $params && $params->{onsite_checkout} ? 1 : 0;
1245 my $auto_renew = $params && $params->{auto_renew};
1246 my $dbh = C4::Context->dbh;
1247 my $barcodecheck = CheckValidBarcode($barcode);
1249 my $issue;
1251 if ( $datedue && ref $datedue ne 'DateTime' ) {
1252 $datedue = dt_from_string($datedue);
1255 # $issuedate defaults to today.
1256 if ( !defined $issuedate ) {
1257 $issuedate = DateTime->now( time_zone => C4::Context->tz() );
1259 else {
1260 if ( ref $issuedate ne 'DateTime' ) {
1261 $issuedate = dt_from_string($issuedate);
1266 # Stop here if the patron or barcode doesn't exist
1267 if ( $borrower && $barcode && $barcodecheck ) {
1268 # find which item we issue
1269 my $item = GetItem( '', $barcode )
1270 or return; # if we don't get an Item, abort.
1272 my $branch = _GetCircControlBranch( $item, $borrower );
1274 # get actual issuing if there is one
1275 my $actualissue = GetItemIssue( $item->{itemnumber} );
1277 # get biblioinformation for this item
1278 my $biblio = GetBiblioFromItemNumber( $item->{itemnumber} );
1280 # check if we just renew the issue.
1281 if ( $actualissue->{borrowernumber} eq $borrower->{'borrowernumber'} ) {
1282 $datedue = AddRenewal(
1283 $borrower->{'borrowernumber'},
1284 $item->{'itemnumber'},
1285 $branch,
1286 $datedue,
1287 $issuedate, # here interpreted as the renewal date
1290 else {
1291 # it's NOT a renewal
1292 if ( $actualissue->{borrowernumber} ) {
1293 # This book is currently on loan, but not to the person
1294 # who wants to borrow it now. mark it returned before issuing to the new borrower
1295 my ( $allowed, $message ) = CanBookBeReturned( $item, C4::Context->userenv->{branch} );
1296 return unless $allowed;
1297 AddReturn( $item->{'barcode'}, C4::Context->userenv->{'branch'} );
1300 MoveReserve( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $cancelreserve );
1302 # Starting process for transfer job (checking transfert and validate it if we have one)
1303 my ($datesent) = GetTransfers( $item->{'itemnumber'} );
1304 if ($datesent) {
1305 # updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....)
1306 my $sth = $dbh->prepare(
1307 "UPDATE branchtransfers
1308 SET datearrived = now(),
1309 tobranch = ?,
1310 comments = 'Forced branchtransfer'
1311 WHERE itemnumber= ? AND datearrived IS NULL"
1313 $sth->execute( C4::Context->userenv->{'branch'},
1314 $item->{'itemnumber'} );
1317 # If automatic renewal wasn't selected while issuing, set the value according to the issuing rule.
1318 unless ($auto_renew) {
1319 my $issuingrule = GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branch );
1320 $auto_renew = $issuingrule->{auto_renew};
1323 # Record in the database the fact that the book was issued.
1324 unless ($datedue) {
1325 my $itype =
1326 ( C4::Context->preference('item-level_itypes') )
1327 ? $biblio->{'itype'}
1328 : $biblio->{'itemtype'};
1329 $datedue = CalcDateDue( $issuedate, $itype, $branch, $borrower );
1332 $datedue->truncate( to => 'minute' );
1334 $issue = Koha::Database->new()->schema()->resultset('Issue')->create(
1336 borrowernumber => $borrower->{'borrowernumber'},
1337 itemnumber => $item->{'itemnumber'},
1338 issuedate => $issuedate->strftime('%Y-%m-%d %H:%M:%S'),
1339 date_due => $datedue->strftime('%Y-%m-%d %H:%M:%S'),
1340 branchcode => C4::Context->userenv->{'branch'},
1341 onsite_checkout => $onsite_checkout,
1342 auto_renew => $auto_renew ? 1 : 0
1346 if ( C4::Context->preference('ReturnToShelvingCart') ) {
1347 # ReturnToShelvingCart is on, anything issued should be taken off the cart.
1348 CartToShelf( $item->{'itemnumber'} );
1350 $item->{'issues'}++;
1351 if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) {
1352 UpdateTotalIssues( $item->{'biblionumber'}, 1 );
1355 ## If item was lost, it has now been found, reverse any list item charges if necessary.
1356 if ( $item->{'itemlost'} ) {
1357 if (
1358 Koha::RefundLostItemFeeRules->should_refund(
1360 current_branch => C4::Context->userenv->{branch},
1361 item_home_branch => $item->{homebranch},
1362 item_holding_branch => $item->{holdingbranch}
1367 _FixAccountForLostAndReturned( $item->{'itemnumber'}, undef,
1368 $item->{'barcode'} );
1372 ModItem(
1374 issues => $item->{'issues'},
1375 holdingbranch => C4::Context->userenv->{'branch'},
1376 itemlost => 0,
1377 onloan => $datedue->ymd(),
1378 datelastborrowed => DateTime->now( time_zone => C4::Context->tz() )->ymd(),
1380 $item->{'biblionumber'},
1381 $item->{'itemnumber'}
1383 ModDateLastSeen( $item->{'itemnumber'} );
1385 # If it costs to borrow this book, charge it to the patron's account.
1386 my ( $charge, $itemtype ) = GetIssuingCharges( $item->{'itemnumber'}, $borrower->{'borrowernumber'} );
1387 if ( $charge > 0 ) {
1388 AddIssuingCharge( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $charge );
1389 $item->{'charge'} = $charge;
1392 # Record the fact that this book was issued.
1393 &UpdateStats(
1395 branch => C4::Context->userenv->{'branch'},
1396 type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
1397 amount => $charge,
1398 other => ( $sipmode ? "SIP-$sipmode" : '' ),
1399 itemnumber => $item->{'itemnumber'},
1400 itemtype => $item->{'itype'},
1401 borrowernumber => $borrower->{'borrowernumber'},
1402 ccode => $item->{'ccode'}
1406 # Send a checkout slip.
1407 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1408 my %conditions = (
1409 branchcode => $branch,
1410 categorycode => $borrower->{categorycode},
1411 item_type => $item->{itype},
1412 notification => 'CHECKOUT',
1414 if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
1415 SendCirculationAlert(
1417 type => 'CHECKOUT',
1418 item => $item,
1419 borrower => $borrower,
1420 branch => $branch,
1426 logaction(
1427 "CIRCULATION", "ISSUE",
1428 $borrower->{'borrowernumber'},
1429 $biblio->{'itemnumber'}
1430 ) if C4::Context->preference("IssueLog");
1432 return $issue;
1435 =head2 GetLoanLength
1437 my $loanlength = &GetLoanLength($borrowertype,$itemtype,branchcode)
1439 Get loan length for an itemtype, a borrower type and a branch
1441 =cut
1443 sub GetLoanLength {
1444 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1445 my $dbh = C4::Context->dbh;
1446 my $sth = $dbh->prepare(qq{
1447 SELECT issuelength, lengthunit, renewalperiod
1448 FROM issuingrules
1449 WHERE categorycode=?
1450 AND itemtype=?
1451 AND branchcode=?
1452 AND issuelength IS NOT NULL
1455 # try to find issuelength & return the 1st available.
1456 # check with borrowertype, itemtype and branchcode, then without one of those parameters
1457 $sth->execute( $borrowertype, $itemtype, $branchcode );
1458 my $loanlength = $sth->fetchrow_hashref;
1460 return $loanlength
1461 if defined($loanlength) && defined $loanlength->{issuelength};
1463 $sth->execute( $borrowertype, '*', $branchcode );
1464 $loanlength = $sth->fetchrow_hashref;
1465 return $loanlength
1466 if defined($loanlength) && defined $loanlength->{issuelength};
1468 $sth->execute( '*', $itemtype, $branchcode );
1469 $loanlength = $sth->fetchrow_hashref;
1470 return $loanlength
1471 if defined($loanlength) && defined $loanlength->{issuelength};
1473 $sth->execute( '*', '*', $branchcode );
1474 $loanlength = $sth->fetchrow_hashref;
1475 return $loanlength
1476 if defined($loanlength) && defined $loanlength->{issuelength};
1478 $sth->execute( $borrowertype, $itemtype, '*' );
1479 $loanlength = $sth->fetchrow_hashref;
1480 return $loanlength
1481 if defined($loanlength) && defined $loanlength->{issuelength};
1483 $sth->execute( $borrowertype, '*', '*' );
1484 $loanlength = $sth->fetchrow_hashref;
1485 return $loanlength
1486 if defined($loanlength) && defined $loanlength->{issuelength};
1488 $sth->execute( '*', $itemtype, '*' );
1489 $loanlength = $sth->fetchrow_hashref;
1490 return $loanlength
1491 if defined($loanlength) && defined $loanlength->{issuelength};
1493 $sth->execute( '*', '*', '*' );
1494 $loanlength = $sth->fetchrow_hashref;
1495 return $loanlength
1496 if defined($loanlength) && defined $loanlength->{issuelength};
1498 # if no rule is set => 0 day (hardcoded)
1499 return {
1500 issuelength => 0,
1501 renewalperiod => 0,
1502 lengthunit => 'days',
1508 =head2 GetHardDueDate
1510 my ($hardduedate,$hardduedatecompare) = &GetHardDueDate($borrowertype,$itemtype,branchcode)
1512 Get the Hard Due Date and it's comparison for an itemtype, a borrower type and a branch
1514 =cut
1516 sub GetHardDueDate {
1517 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1519 my $rule = GetIssuingRule( $borrowertype, $itemtype, $branchcode );
1521 if ( defined( $rule ) ) {
1522 if ( $rule->{hardduedate} ) {
1523 return (dt_from_string($rule->{hardduedate}, 'iso'),$rule->{hardduedatecompare});
1524 } else {
1525 return (undef, undef);
1530 =head2 GetIssuingRule
1532 my $irule = &GetIssuingRule($borrowertype,$itemtype,branchcode)
1534 FIXME - This is a copy-paste of GetLoanLength
1535 as a stop-gap. Do not wish to change API for GetLoanLength
1536 this close to release.
1538 Get the issuing rule for an itemtype, a borrower type and a branch
1539 Returns a hashref from the issuingrules table.
1541 =cut
1543 sub GetIssuingRule {
1544 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1545 my $dbh = C4::Context->dbh;
1546 my $sth = $dbh->prepare( "select * from issuingrules where categorycode=? and itemtype=? and branchcode=?" );
1547 my $irule;
1549 $sth->execute( $borrowertype, $itemtype, $branchcode );
1550 $irule = $sth->fetchrow_hashref;
1551 return $irule if defined($irule) ;
1553 $sth->execute( $borrowertype, "*", $branchcode );
1554 $irule = $sth->fetchrow_hashref;
1555 return $irule if defined($irule) ;
1557 $sth->execute( "*", $itemtype, $branchcode );
1558 $irule = $sth->fetchrow_hashref;
1559 return $irule if defined($irule) ;
1561 $sth->execute( "*", "*", $branchcode );
1562 $irule = $sth->fetchrow_hashref;
1563 return $irule if defined($irule) ;
1565 $sth->execute( $borrowertype, $itemtype, "*" );
1566 $irule = $sth->fetchrow_hashref;
1567 return $irule if defined($irule) ;
1569 $sth->execute( $borrowertype, "*", "*" );
1570 $irule = $sth->fetchrow_hashref;
1571 return $irule if defined($irule) ;
1573 $sth->execute( "*", $itemtype, "*" );
1574 $irule = $sth->fetchrow_hashref;
1575 return $irule if defined($irule) ;
1577 $sth->execute( "*", "*", "*" );
1578 $irule = $sth->fetchrow_hashref;
1579 return $irule if defined($irule) ;
1581 # if no rule matches,
1582 return;
1585 =head2 GetBranchBorrowerCircRule
1587 my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
1589 Retrieves circulation rule attributes that apply to the given
1590 branch and patron category, regardless of item type.
1591 The return value is a hashref containing the following key:
1593 maxissueqty - maximum number of loans that a
1594 patron of the given category can have at the given
1595 branch. If the value is undef, no limit.
1597 maxonsiteissueqty - maximum of on-site checkouts that a
1598 patron of the given category can have at the given
1599 branch. If the value is undef, no limit.
1601 This will first check for a specific branch and
1602 category match from branch_borrower_circ_rules.
1604 If no rule is found, it will then check default_branch_circ_rules
1605 (same branch, default category). If no rule is found,
1606 it will then check default_borrower_circ_rules (default
1607 branch, same category), then failing that, default_circ_rules
1608 (default branch, default category).
1610 If no rule has been found in the database, it will default to
1611 the buillt in rule:
1613 maxissueqty - undef
1614 maxonsiteissueqty - undef
1616 C<$branchcode> and C<$categorycode> should contain the
1617 literal branch code and patron category code, respectively - no
1618 wildcards.
1620 =cut
1622 sub GetBranchBorrowerCircRule {
1623 my ( $branchcode, $categorycode ) = @_;
1625 my $rules;
1626 my $dbh = C4::Context->dbh();
1627 $rules = $dbh->selectrow_hashref( q|
1628 SELECT maxissueqty, maxonsiteissueqty
1629 FROM branch_borrower_circ_rules
1630 WHERE branchcode = ?
1631 AND categorycode = ?
1632 |, {}, $branchcode, $categorycode ) ;
1633 return $rules if $rules;
1635 # try same branch, default borrower category
1636 $rules = $dbh->selectrow_hashref( q|
1637 SELECT maxissueqty, maxonsiteissueqty
1638 FROM default_branch_circ_rules
1639 WHERE branchcode = ?
1640 |, {}, $branchcode ) ;
1641 return $rules if $rules;
1643 # try default branch, same borrower category
1644 $rules = $dbh->selectrow_hashref( q|
1645 SELECT maxissueqty, maxonsiteissueqty
1646 FROM default_borrower_circ_rules
1647 WHERE categorycode = ?
1648 |, {}, $categorycode ) ;
1649 return $rules if $rules;
1651 # try default branch, default borrower category
1652 $rules = $dbh->selectrow_hashref( q|
1653 SELECT maxissueqty, maxonsiteissueqty
1654 FROM default_circ_rules
1655 |, {} );
1656 return $rules if $rules;
1658 # built-in default circulation rule
1659 return {
1660 maxissueqty => undef,
1661 maxonsiteissueqty => undef,
1665 =head2 GetBranchItemRule
1667 my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
1669 Retrieves circulation rule attributes that apply to the given
1670 branch and item type, regardless of patron category.
1672 The return value is a hashref containing the following keys:
1674 holdallowed => Hold policy for this branch and itemtype. Possible values:
1675 0: No holds allowed.
1676 1: Holds allowed only by patrons that have the same homebranch as the item.
1677 2: Holds allowed from any patron.
1679 returnbranch => branch to which to return item. Possible values:
1680 noreturn: do not return, let item remain where checked in (floating collections)
1681 homebranch: return to item's home branch
1682 holdingbranch: return to issuer branch
1684 This searches branchitemrules in the following order:
1686 * Same branchcode and itemtype
1687 * Same branchcode, itemtype '*'
1688 * branchcode '*', same itemtype
1689 * branchcode and itemtype '*'
1691 Neither C<$branchcode> nor C<$itemtype> should be '*'.
1693 =cut
1695 sub GetBranchItemRule {
1696 my ( $branchcode, $itemtype ) = @_;
1697 my $dbh = C4::Context->dbh();
1698 my $result = {};
1700 my @attempts = (
1701 ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1702 FROM branch_item_rules
1703 WHERE branchcode = ?
1704 AND itemtype = ?', $branchcode, $itemtype],
1705 ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1706 FROM default_branch_circ_rules
1707 WHERE branchcode = ?', $branchcode],
1708 ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1709 FROM default_branch_item_rules
1710 WHERE itemtype = ?', $itemtype],
1711 ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1712 FROM default_circ_rules'],
1715 foreach my $attempt (@attempts) {
1716 my ($query, @bind_params) = @{$attempt};
1717 my $search_result = $dbh->selectrow_hashref ( $query , {}, @bind_params )
1718 or next;
1720 # Since branch/category and branch/itemtype use the same per-branch
1721 # defaults tables, we have to check that the key we want is set, not
1722 # just that a row was returned
1723 $result->{'holdallowed'} = $search_result->{'holdallowed'} unless ( defined $result->{'holdallowed'} );
1724 $result->{'hold_fulfillment_policy'} = $search_result->{'hold_fulfillment_policy'} unless ( defined $result->{'hold_fulfillment_policy'} );
1725 $result->{'returnbranch'} = $search_result->{'returnbranch'} unless ( defined $result->{'returnbranch'} );
1728 # built-in default circulation rule
1729 $result->{'holdallowed'} = 2 unless ( defined $result->{'holdallowed'} );
1730 $result->{'hold_fulfillment_policy'} = 'any' unless ( defined $result->{'hold_fulfillment_policy'} );
1731 $result->{'returnbranch'} = 'homebranch' unless ( defined $result->{'returnbranch'} );
1733 return $result;
1736 =head2 AddReturn
1738 ($doreturn, $messages, $iteminformation, $borrower) =
1739 &AddReturn( $barcode, $branch [,$exemptfine] [,$dropbox] [,$returndate] );
1741 Returns a book.
1743 =over 4
1745 =item C<$barcode> is the bar code of the book being returned.
1747 =item C<$branch> is the code of the branch where the book is being returned.
1749 =item C<$exemptfine> indicates that overdue charges for the item will be
1750 removed. Optional.
1752 =item C<$dropbox> indicates that the check-in date is assumed to be
1753 yesterday, or the last non-holiday as defined in C4::Calendar . If
1754 overdue charges are applied and C<$dropbox> is true, the last charge
1755 will be removed. This assumes that the fines accrual script has run
1756 for _today_. Optional.
1758 =item C<$return_date> allows the default return date to be overridden
1759 by the given return date. Optional.
1761 =back
1763 C<&AddReturn> returns a list of four items:
1765 C<$doreturn> is true iff the return succeeded.
1767 C<$messages> is a reference-to-hash giving feedback on the operation.
1768 The keys of the hash are:
1770 =over 4
1772 =item C<BadBarcode>
1774 No item with this barcode exists. The value is C<$barcode>.
1776 =item C<NotIssued>
1778 The book is not currently on loan. The value is C<$barcode>.
1780 =item C<IsPermanent>
1782 The book's home branch is a permanent collection. If you have borrowed
1783 this book, you are not allowed to return it. The value is the code for
1784 the book's home branch.
1786 =item C<withdrawn>
1788 This book has been withdrawn/cancelled. The value should be ignored.
1790 =item C<Wrongbranch>
1792 This book has was returned to the wrong branch. The value is a hashref
1793 so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
1794 contain the branchcode of the incorrect and correct return library, respectively.
1796 =item C<ResFound>
1798 The item was reserved. The value is a reference-to-hash whose keys are
1799 fields from the reserves table of the Koha database, and
1800 C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
1801 either C<Waiting>, C<Reserved>, or 0.
1803 =item C<WasReturned>
1805 Value 1 if return is successful.
1807 =item C<NeedsTransfer>
1809 If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer.
1811 =back
1813 C<$iteminformation> is a reference-to-hash, giving information about the
1814 returned item from the issues table.
1816 C<$borrower> is a reference-to-hash, giving information about the
1817 patron who last borrowed the book.
1819 =cut
1821 sub AddReturn {
1822 my ( $barcode, $branch, $exemptfine, $dropbox, $return_date, $dropboxdate ) = @_;
1824 if ($branch and not Koha::Libraries->find($branch)) {
1825 warn "AddReturn error: branch '$branch' not found. Reverting to " . C4::Context->userenv->{'branch'};
1826 undef $branch;
1828 $branch = C4::Context->userenv->{'branch'} unless $branch; # we trust userenv to be a safe fallback/default
1829 my $messages;
1830 my $borrower;
1831 my $biblio;
1832 my $doreturn = 1;
1833 my $validTransfert = 0;
1834 my $stat_type = 'return';
1836 # get information on item
1837 my $itemnumber = GetItemnumberFromBarcode( $barcode );
1838 unless ($itemnumber) {
1839 return (0, { BadBarcode => $barcode }); # no barcode means no item or borrower. bail out.
1841 my $issue = GetItemIssue($itemnumber);
1842 if ($issue and $issue->{borrowernumber}) {
1843 $borrower = C4::Members::GetMemberDetails($issue->{borrowernumber})
1844 or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existent borrowernumber '$issue->{borrowernumber}'\n"
1845 . Dumper($issue) . "\n";
1846 } else {
1847 $messages->{'NotIssued'} = $barcode;
1848 # even though item is not on loan, it may still be transferred; therefore, get current branch info
1849 $doreturn = 0;
1850 # No issue, no borrowernumber. ONLY if $doreturn, *might* you have a $borrower later.
1851 # Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
1852 if (C4::Context->preference("RecordLocalUseOnReturn")) {
1853 $messages->{'LocalUse'} = 1;
1854 $stat_type = 'localuse';
1858 my $item = GetItem($itemnumber) or die "GetItem($itemnumber) failed";
1860 if ( $item->{'location'} eq 'PROC' ) {
1861 if ( C4::Context->preference("InProcessingToShelvingCart") ) {
1862 $item->{'location'} = 'CART';
1864 else {
1865 $item->{location} = $item->{permanent_location};
1868 ModItem( $item, $item->{'biblionumber'}, $item->{'itemnumber'} );
1871 # full item data, but no borrowernumber or checkout info (no issue)
1872 # we know GetItem should work because GetItemnumberFromBarcode worked
1873 my $hbr = GetBranchItemRule($item->{'homebranch'}, $item->{'itype'})->{'returnbranch'} || "homebranch";
1874 # get the proper branch to which to return the item
1875 my $returnbranch = $item->{$hbr} || $branch ;
1876 # if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
1878 my $borrowernumber = $borrower->{'borrowernumber'} || undef; # we don't know if we had a borrower or not
1880 my $yaml = C4::Context->preference('UpdateNotForLoanStatusOnCheckin');
1881 if ($yaml) {
1882 $yaml = "$yaml\n\n"; # YAML is anal on ending \n. Surplus does not hurt
1883 my $rules;
1884 eval { $rules = YAML::Load($yaml); };
1885 if ($@) {
1886 warn "Unable to parse UpdateNotForLoanStatusOnCheckin syspref : $@";
1888 else {
1889 foreach my $key ( keys %$rules ) {
1890 if ( $item->{notforloan} eq $key ) {
1891 $messages->{'NotForLoanStatusUpdated'} = { from => $item->{notforloan}, to => $rules->{$key} };
1892 ModItem( { notforloan => $rules->{$key} }, undef, $itemnumber );
1893 last;
1900 # check if the book is in a permanent collection....
1901 # FIXME -- This 'PE' attribute is largely undocumented. afaict, there's no user interface that reflects this functionality.
1902 if ( $returnbranch ) {
1903 my $library = Koha::Libraries->find($returnbranch);
1904 if ( $library and $library->get_categories->search({'me.categorycode' => 'PE'})->count ) {
1905 $messages->{'IsPermanent'} = $returnbranch;
1909 # check if the return is allowed at this branch
1910 my ($returnallowed, $message) = CanBookBeReturned($item, $branch);
1911 unless ($returnallowed){
1912 $messages->{'Wrongbranch'} = {
1913 Wrongbranch => $branch,
1914 Rightbranch => $message
1916 $doreturn = 0;
1917 return ( $doreturn, $messages, $issue, $borrower );
1920 if ( $item->{'withdrawn'} ) { # book has been cancelled
1921 $messages->{'withdrawn'} = 1;
1922 $doreturn = 0 if C4::Context->preference("BlockReturnOfWithdrawnItems");
1925 # case of a return of document (deal with issues and holdingbranch)
1926 my $today = DateTime->now( time_zone => C4::Context->tz() );
1928 if ($doreturn) {
1929 my $datedue = $issue->{date_due};
1930 $borrower or warn "AddReturn without current borrower";
1931 my $circControlBranch;
1932 if ($dropbox) {
1933 # define circControlBranch only if dropbox mode is set
1934 # don't allow dropbox mode to create an invalid entry in issues (issuedate > today)
1935 # FIXME: check issuedate > returndate, factoring in holidays
1937 $circControlBranch = _GetCircControlBranch($item,$borrower);
1938 $issue->{'overdue'} = DateTime->compare($issue->{'date_due'}, $dropboxdate ) == -1 ? 1 : 0;
1941 if ($borrowernumber) {
1942 if ( ( C4::Context->preference('CalculateFinesOnReturn') && $issue->{'overdue'} ) || $return_date ) {
1943 _CalculateAndUpdateFine( { issue => $issue, item => $item, borrower => $borrower, return_date => $return_date } );
1946 eval {
1947 MarkIssueReturned( $borrowernumber, $item->{'itemnumber'},
1948 $circControlBranch, $return_date, $borrower->{'privacy'} );
1950 if ( $@ ) {
1951 $messages->{'Wrongbranch'} = {
1952 Wrongbranch => $branch,
1953 Rightbranch => $message
1955 carp $@;
1956 return ( 0, { WasReturned => 0 }, $issue, $borrower );
1959 # FIXME is the "= 1" right? This could be the borrower hash.
1960 $messages->{'WasReturned'} = 1;
1964 ModItem({ onloan => undef }, $issue->{'biblionumber'}, $item->{'itemnumber'});
1967 # the holdingbranch is updated if the document is returned to another location.
1968 # this is always done regardless of whether the item was on loan or not
1969 my $item_holding_branch = $item->{ holdingbranch };
1970 if ($item->{'holdingbranch'} ne $branch) {
1971 UpdateHoldingbranch($branch, $item->{'itemnumber'});
1972 $item->{'holdingbranch'} = $branch; # update item data holdingbranch too
1974 ModDateLastSeen( $item->{'itemnumber'} );
1976 # check if we have a transfer for this document
1977 my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->{'itemnumber'} );
1979 # if we have a transfer to do, we update the line of transfers with the datearrived
1980 my $is_in_rotating_collection = C4::RotatingCollections::isItemInAnyCollection( $item->{'itemnumber'} );
1981 if ($datesent) {
1982 if ( $tobranch eq $branch ) {
1983 my $sth = C4::Context->dbh->prepare(
1984 "UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL"
1986 $sth->execute( $item->{'itemnumber'} );
1987 # if we have a reservation with valid transfer, we can set it's status to 'W'
1988 ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
1989 C4::Reserves::ModReserveStatus($item->{'itemnumber'}, 'W');
1990 } else {
1991 $messages->{'WrongTransfer'} = $tobranch;
1992 $messages->{'WrongTransferItem'} = $item->{'itemnumber'};
1994 $validTransfert = 1;
1995 } else {
1996 ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
1999 # fix up the accounts.....
2000 if ( $item->{'itemlost'} ) {
2001 $messages->{'WasLost'} = 1;
2003 if ( $item->{'itemlost'} ) {
2004 if (
2005 Koha::RefundLostItemFeeRules->should_refund(
2007 current_branch => C4::Context->userenv->{branch},
2008 item_home_branch => $item->{homebranch},
2009 item_holding_branch => $item_holding_branch
2014 _FixAccountForLostAndReturned( $item->{'itemnumber'}, $borrowernumber, $barcode );
2015 $messages->{'LostItemFeeRefunded'} = 1;
2020 # fix up the overdues in accounts...
2021 if ($borrowernumber) {
2022 my $fix = _FixOverduesOnReturn($borrowernumber, $item->{itemnumber}, $exemptfine, $dropbox);
2023 defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $item->{itemnumber}...) failed!"; # zero is OK, check defined
2025 if ( $issue->{overdue} && $issue->{date_due} ) {
2026 # fix fine days
2027 $today = $dropboxdate if $dropbox;
2028 my ($debardate,$reminder) = _debar_user_on_return( $borrower, $item, $issue->{date_due}, $today );
2029 if ($reminder){
2030 $messages->{'PrevDebarred'} = $debardate;
2031 } else {
2032 $messages->{'Debarred'} = $debardate if $debardate;
2034 # there's no overdue on the item but borrower had been previously debarred
2035 } elsif ( $issue->{date_due} and $borrower->{'debarred'} ) {
2036 if ( $borrower->{debarred} eq "9999-12-31") {
2037 $messages->{'ForeverDebarred'} = $borrower->{'debarred'};
2038 } else {
2039 my $borrower_debar_dt = dt_from_string( $borrower->{debarred} );
2040 $borrower_debar_dt->truncate(to => 'day');
2041 my $today_dt = $today->clone()->truncate(to => 'day');
2042 if ( DateTime->compare( $borrower_debar_dt, $today_dt ) != -1 ) {
2043 $messages->{'PrevDebarred'} = $borrower->{'debarred'};
2049 # find reserves.....
2050 # if we don't have a reserve with the status W, we launch the Checkreserves routine
2051 my ($resfound, $resrec);
2052 my $lookahead= C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
2053 ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->{'itemnumber'}, undef, $lookahead ) unless ( $item->{'withdrawn'} );
2054 if ($resfound) {
2055 $resrec->{'ResFound'} = $resfound;
2056 $messages->{'ResFound'} = $resrec;
2059 # Record the fact that this book was returned.
2060 # FIXME itemtype should record item level type, not bibliolevel type
2061 UpdateStats({
2062 branch => $branch,
2063 type => $stat_type,
2064 itemnumber => $item->{'itemnumber'},
2065 itemtype => $biblio->{'itemtype'},
2066 borrowernumber => $borrowernumber,
2067 ccode => $item->{'ccode'}}
2070 # Send a check-in slip. # NOTE: borrower may be undef. probably shouldn't try to send messages then.
2071 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2072 my %conditions = (
2073 branchcode => $branch,
2074 categorycode => $borrower->{categorycode},
2075 item_type => $item->{itype},
2076 notification => 'CHECKIN',
2078 if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
2079 SendCirculationAlert({
2080 type => 'CHECKIN',
2081 item => $item,
2082 borrower => $borrower,
2083 branch => $branch,
2087 logaction("CIRCULATION", "RETURN", $borrowernumber, $item->{'itemnumber'})
2088 if C4::Context->preference("ReturnLog");
2090 # Remove any OVERDUES related debarment if the borrower has no overdues
2091 if ( $borrowernumber
2092 && $borrower->{'debarred'}
2093 && C4::Context->preference('AutoRemoveOverduesRestrictions')
2094 && !Koha::Patrons->find( $borrowernumber )->has_overdues
2095 && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2097 DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2100 # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
2101 if (!$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) and not $messages->{'WrongTransfer'}){
2102 if (C4::Context->preference("AutomaticItemReturn" ) or
2103 (C4::Context->preference("UseBranchTransferLimits") and
2104 ! IsBranchTransferAllowed($branch, $returnbranch, $item->{C4::Context->preference("BranchTransferLimitsType")} )
2105 )) {
2106 $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s)", $item->{'itemnumber'},$branch, $returnbranch;
2107 $debug and warn "item: " . Dumper($item);
2108 ModItemTransfer($item->{'itemnumber'}, $branch, $returnbranch);
2109 $messages->{'WasTransfered'} = 1;
2110 } else {
2111 $messages->{'NeedsTransfer'} = $returnbranch;
2115 return ( $doreturn, $messages, $issue, $borrower );
2118 =head2 MarkIssueReturned
2120 MarkIssueReturned($borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy);
2122 Unconditionally marks an issue as being returned by
2123 moving the C<issues> row to C<old_issues> and
2124 setting C<returndate> to the current date, or
2125 the last non-holiday date of the branccode specified in
2126 C<dropbox_branch> . Assumes you've already checked that
2127 it's safe to do this, i.e. last non-holiday > issuedate.
2129 if C<$returndate> is specified (in iso format), it is used as the date
2130 of the return. It is ignored when a dropbox_branch is passed in.
2132 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
2133 the old_issue is immediately anonymised
2135 Ideally, this function would be internal to C<C4::Circulation>,
2136 not exported, but it is currently needed by one
2137 routine in C<C4::Accounts>.
2139 =cut
2141 sub MarkIssueReturned {
2142 my ( $borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy ) = @_;
2144 my $anonymouspatron;
2145 if ( $privacy == 2 ) {
2146 # The default of 0 will not work due to foreign key constraints
2147 # The anonymisation will fail if AnonymousPatron is not a valid entry
2148 # We need to check if the anonymous patron exist, Koha will fail loudly if it does not
2149 # Note that a warning should appear on the about page (System information tab).
2150 $anonymouspatron = C4::Context->preference('AnonymousPatron');
2151 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."
2152 unless C4::Members::GetMember( borrowernumber => $anonymouspatron );
2154 my $dbh = C4::Context->dbh;
2155 my $query = 'UPDATE issues SET returndate=';
2156 my @bind;
2157 if ($dropbox_branch) {
2158 my $calendar = Koha::Calendar->new( branchcode => $dropbox_branch );
2159 my $dropboxdate = $calendar->addDate( DateTime->now( time_zone => C4::Context->tz), -1 );
2160 $query .= ' ? ';
2161 push @bind, $dropboxdate->strftime('%Y-%m-%d %H:%M');
2162 } elsif ($returndate) {
2163 $query .= ' ? ';
2164 push @bind, $returndate;
2165 } else {
2166 $query .= ' now() ';
2168 $query .= ' WHERE borrowernumber = ? AND itemnumber = ?';
2169 push @bind, $borrowernumber, $itemnumber;
2170 # FIXME transaction
2171 my $sth_upd = $dbh->prepare($query);
2172 $sth_upd->execute(@bind);
2173 my $sth_copy = $dbh->prepare('INSERT INTO old_issues SELECT * FROM issues
2174 WHERE borrowernumber = ?
2175 AND itemnumber = ?');
2176 $sth_copy->execute($borrowernumber, $itemnumber);
2177 # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
2178 if ( $privacy == 2) {
2179 my $sth_ano = $dbh->prepare("UPDATE old_issues SET borrowernumber=?
2180 WHERE borrowernumber = ?
2181 AND itemnumber = ?");
2182 $sth_ano->execute($anonymouspatron, $borrowernumber, $itemnumber);
2184 my $sth_del = $dbh->prepare("DELETE FROM issues
2185 WHERE borrowernumber = ?
2186 AND itemnumber = ?");
2187 $sth_del->execute($borrowernumber, $itemnumber);
2189 ModItem( { 'onloan' => undef }, undef, $itemnumber );
2191 if ( C4::Context->preference('StoreLastBorrower') ) {
2192 my $item = Koha::Items->find( $itemnumber );
2193 my $patron = Koha::Patrons->find( $borrowernumber );
2194 $item->last_returned_by( $patron );
2198 =head2 _debar_user_on_return
2200 _debar_user_on_return($borrower, $item, $datedue, today);
2202 C<$borrower> borrower hashref
2204 C<$item> item hashref
2206 C<$datedue> date due DateTime object
2208 C<$today> DateTime object representing the return time
2210 Internal function, called only by AddReturn that calculates and updates
2211 the user fine days, and debars him if necessary.
2213 Should only be called for overdue returns
2215 =cut
2217 sub _debar_user_on_return {
2218 my ( $borrower, $item, $dt_due, $dt_today ) = @_;
2220 my $branchcode = _GetCircControlBranch( $item, $borrower );
2222 my $circcontrol = C4::Context->preference('CircControl');
2223 my $issuingrule =
2224 GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode );
2225 my $finedays = $issuingrule->{finedays};
2226 my $unit = $issuingrule->{lengthunit};
2227 my $chargeable_units = C4::Overdues::get_chargeable_units($unit, $dt_due, $dt_today, $branchcode);
2229 if ($finedays) {
2231 # finedays is in days, so hourly loans must multiply by 24
2232 # thus 1 hour late equals 1 day suspension * finedays rate
2233 $finedays = $finedays * 24 if ( $unit eq 'hours' );
2235 # grace period is measured in the same units as the loan
2236 my $grace =
2237 DateTime::Duration->new( $unit => $issuingrule->{firstremind} );
2239 my $deltadays = DateTime::Duration->new(
2240 days => $chargeable_units
2242 if ( $deltadays->subtract($grace)->is_positive() ) {
2243 my $suspension_days = $deltadays * $finedays;
2245 # If the max suspension days is < than the suspension days
2246 # the suspension days is limited to this maximum period.
2247 my $max_sd = $issuingrule->{maxsuspensiondays};
2248 if ( defined $max_sd ) {
2249 $max_sd = DateTime::Duration->new( days => $max_sd );
2250 $suspension_days = $max_sd
2251 if DateTime::Duration->compare( $max_sd, $suspension_days ) < 0;
2254 my $new_debar_dt =
2255 $dt_today->clone()->add_duration( $suspension_days );
2257 Koha::Patron::Debarments::AddUniqueDebarment({
2258 borrowernumber => $borrower->{borrowernumber},
2259 expiration => $new_debar_dt->ymd(),
2260 type => 'SUSPENSION',
2262 # if borrower was already debarred but does not get an extra debarment
2263 my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
2264 if ( $borrower->{debarred} eq $patron->is_debarred ) {
2265 return ($borrower->{debarred},1);
2267 return $new_debar_dt->ymd();
2270 return;
2273 =head2 _FixOverduesOnReturn
2275 &_FixOverduesOnReturn($brn,$itm, $exemptfine, $dropboxmode);
2277 C<$brn> borrowernumber
2279 C<$itm> itemnumber
2281 C<$exemptfine> BOOL -- remove overdue charge associated with this issue.
2282 C<$dropboxmode> BOOL -- remove lastincrement on overdue charge associated with this issue.
2284 Internal function, called only by AddReturn
2286 =cut
2288 sub _FixOverduesOnReturn {
2289 my ($borrowernumber, $item);
2290 unless ($borrowernumber = shift) {
2291 warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
2292 return;
2294 unless ($item = shift) {
2295 warn "_FixOverduesOnReturn() not supplied valid itemnumber";
2296 return;
2298 my ($exemptfine, $dropbox) = @_;
2299 my $dbh = C4::Context->dbh;
2301 # check for overdue fine
2302 my $sth = $dbh->prepare(
2303 "SELECT * FROM accountlines WHERE (borrowernumber = ?) AND (itemnumber = ?) AND (accounttype='FU' OR accounttype='O')"
2305 $sth->execute( $borrowernumber, $item );
2307 # alter fine to show that the book has been returned
2308 my $data = $sth->fetchrow_hashref;
2309 return 0 unless $data; # no warning, there's just nothing to fix
2311 my $uquery;
2312 my @bind = ($data->{'accountlines_id'});
2313 if ($exemptfine) {
2314 $uquery = "update accountlines set accounttype='FFOR', amountoutstanding=0";
2315 if (C4::Context->preference("FinesLog")) {
2316 &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
2318 } elsif ($dropbox && $data->{lastincrement}) {
2319 my $outstanding = $data->{amountoutstanding} - $data->{lastincrement} ;
2320 my $amt = $data->{amount} - $data->{lastincrement} ;
2321 if (C4::Context->preference("FinesLog")) {
2322 &logaction("FINES", 'MODIFY',$borrowernumber,"Dropbox adjustment $amt, item $item");
2324 $uquery = "update accountlines set accounttype='F' ";
2325 if($outstanding >= 0 && $amt >=0) {
2326 $uquery .= ", amount = ? , amountoutstanding=? ";
2327 unshift @bind, ($amt, $outstanding) ;
2329 } else {
2330 $uquery = "update accountlines set accounttype='F' ";
2332 $uquery .= " where (accountlines_id = ?)";
2333 my $usth = $dbh->prepare($uquery);
2334 return $usth->execute(@bind);
2337 =head2 _FixAccountForLostAndReturned
2339 &_FixAccountForLostAndReturned($itemnumber, [$borrowernumber, $barcode]);
2341 Calculates the charge for a book lost and returned.
2343 Internal function, not exported, called only by AddReturn.
2345 FIXME: This function reflects how inscrutable fines logic is. Fix both.
2346 FIXME: Give a positive return value on success. It might be the $borrowernumber who received credit, or the amount forgiven.
2348 =cut
2350 sub _FixAccountForLostAndReturned {
2351 my $itemnumber = shift or return;
2352 my $borrowernumber = @_ ? shift : undef;
2353 my $item_id = @_ ? shift : $itemnumber; # Send the barcode if you want that logged in the description
2354 my $dbh = C4::Context->dbh;
2355 # check for charge made for lost book
2356 my $sth = $dbh->prepare("SELECT * FROM accountlines WHERE itemnumber = ? AND accounttype IN ('L', 'Rep', 'W') ORDER BY date DESC, accountno DESC");
2357 $sth->execute($itemnumber);
2358 my $data = $sth->fetchrow_hashref;
2359 $data or return; # bail if there is nothing to do
2360 $data->{accounttype} eq 'W' and return; # Written off
2362 # writeoff this amount
2363 my $offset;
2364 my $amount = $data->{'amount'};
2365 my $acctno = $data->{'accountno'};
2366 my $amountleft; # Starts off undef/zero.
2367 if ($data->{'amountoutstanding'} == $amount) {
2368 $offset = $data->{'amount'};
2369 $amountleft = 0; # Hey, it's zero here, too.
2370 } else {
2371 $offset = $amount - $data->{'amountoutstanding'}; # Um, isn't this the same as ZERO? We just tested those two things are ==
2372 $amountleft = $data->{'amountoutstanding'} - $amount; # Um, isn't this the same as ZERO? We just tested those two things are ==
2374 my $usth = $dbh->prepare("UPDATE accountlines SET accounttype = 'LR',amountoutstanding='0'
2375 WHERE (accountlines_id = ?)");
2376 $usth->execute($data->{'accountlines_id'}); # We might be adjusting an account for some OTHER borrowernumber now. Not the one we passed in.
2377 #check if any credit is left if so writeoff other accounts
2378 my $nextaccntno = getnextacctno($data->{'borrowernumber'});
2379 $amountleft *= -1 if ($amountleft < 0);
2380 if ($amountleft > 0) {
2381 my $msth = $dbh->prepare("SELECT * FROM accountlines WHERE (borrowernumber = ?)
2382 AND (amountoutstanding >0) ORDER BY date"); # might want to order by amountoustanding ASC (pay smallest first)
2383 $msth->execute($data->{'borrowernumber'});
2384 # offset transactions
2385 my $newamtos;
2386 my $accdata;
2387 while (($accdata=$msth->fetchrow_hashref) and ($amountleft>0)){
2388 if ($accdata->{'amountoutstanding'} < $amountleft) {
2389 $newamtos = 0;
2390 $amountleft -= $accdata->{'amountoutstanding'};
2391 } else {
2392 $newamtos = $accdata->{'amountoutstanding'} - $amountleft;
2393 $amountleft = 0;
2395 my $thisacct = $accdata->{'accountlines_id'};
2396 # FIXME: move prepares outside while loop!
2397 my $usth = $dbh->prepare("UPDATE accountlines SET amountoutstanding= ?
2398 WHERE (accountlines_id = ?)");
2399 $usth->execute($newamtos,$thisacct);
2400 $usth = $dbh->prepare("INSERT INTO accountoffsets
2401 (borrowernumber, accountno, offsetaccount, offsetamount)
2402 VALUES
2403 (?,?,?,?)");
2404 $usth->execute($data->{'borrowernumber'},$accdata->{'accountno'},$nextaccntno,$newamtos);
2407 $amountleft *= -1 if ($amountleft > 0);
2408 my $desc = "Item Returned " . $item_id;
2409 $usth = $dbh->prepare("INSERT INTO accountlines
2410 (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
2411 VALUES (?,?,now(),?,?,'CR',?)");
2412 $usth->execute($data->{'borrowernumber'},$nextaccntno,0-$amount,$desc,$amountleft);
2413 if ($borrowernumber) {
2414 # FIXME: same as query above. use 1 sth for both
2415 $usth = $dbh->prepare("INSERT INTO accountoffsets
2416 (borrowernumber, accountno, offsetaccount, offsetamount)
2417 VALUES (?,?,?,?)");
2418 $usth->execute($borrowernumber, $data->{'accountno'}, $nextaccntno, $offset);
2420 ModItem({ paidfor => '' }, undef, $itemnumber);
2421 return;
2424 =head2 _GetCircControlBranch
2426 my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
2428 Internal function :
2430 Return the library code to be used to determine which circulation
2431 policy applies to a transaction. Looks up the CircControl and
2432 HomeOrHoldingBranch system preferences.
2434 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
2436 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
2438 =cut
2440 sub _GetCircControlBranch {
2441 my ($item, $borrower) = @_;
2442 my $circcontrol = C4::Context->preference('CircControl');
2443 my $branch;
2445 if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
2446 $branch= C4::Context->userenv->{'branch'};
2447 } elsif ($circcontrol eq 'PatronLibrary') {
2448 $branch=$borrower->{branchcode};
2449 } else {
2450 my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
2451 $branch = $item->{$branchfield};
2452 # default to item home branch if holdingbranch is used
2453 # and is not defined
2454 if (!defined($branch) && $branchfield eq 'holdingbranch') {
2455 $branch = $item->{homebranch};
2458 return $branch;
2466 =head2 GetItemIssue
2468 $issue = &GetItemIssue($itemnumber);
2470 Returns patron currently having a book, or undef if not checked out.
2472 C<$itemnumber> is the itemnumber.
2474 C<$issue> is a hashref of the row from the issues table.
2476 =cut
2478 sub GetItemIssue {
2479 my ($itemnumber) = @_;
2480 return unless $itemnumber;
2481 my $sth = C4::Context->dbh->prepare(
2482 "SELECT items.*, issues.*
2483 FROM issues
2484 LEFT JOIN items ON issues.itemnumber=items.itemnumber
2485 WHERE issues.itemnumber=?");
2486 $sth->execute($itemnumber);
2487 my $data = $sth->fetchrow_hashref;
2488 return unless $data;
2489 $data->{issuedate_sql} = $data->{issuedate};
2490 $data->{date_due_sql} = $data->{date_due};
2491 $data->{issuedate} = dt_from_string($data->{issuedate}, 'sql');
2492 $data->{issuedate}->truncate(to => 'minute');
2493 $data->{date_due} = dt_from_string($data->{date_due}, 'sql');
2494 $data->{date_due}->truncate(to => 'minute');
2495 my $dt = DateTime->now( time_zone => C4::Context->tz)->truncate( to => 'minute');
2496 $data->{'overdue'} = DateTime->compare($data->{'date_due'}, $dt ) == -1 ? 1 : 0;
2497 return $data;
2500 =head2 GetOpenIssue
2502 $issue = GetOpenIssue( $itemnumber );
2504 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2506 C<$itemnumber> is the item's itemnumber
2508 Returns a hashref
2510 =cut
2512 sub GetOpenIssue {
2513 my ( $itemnumber ) = @_;
2514 return unless $itemnumber;
2515 my $dbh = C4::Context->dbh;
2516 my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2517 $sth->execute( $itemnumber );
2518 return $sth->fetchrow_hashref();
2522 =head2 GetIssues
2524 $issues = GetIssues({}); # return all issues!
2525 $issues = GetIssues({ borrowernumber => $borrowernumber, biblionumber => $biblionumber });
2527 Returns all pending issues that match given criteria.
2528 Returns a arrayref or undef if an error occurs.
2530 Allowed criteria are:
2532 =over 2
2534 =item * borrowernumber
2536 =item * biblionumber
2538 =item * itemnumber
2540 =back
2542 =cut
2544 sub GetIssues {
2545 my ($criteria) = @_;
2547 # Build filters
2548 my @filters;
2549 my @allowed = qw(borrowernumber biblionumber itemnumber);
2550 foreach (@allowed) {
2551 if (defined $criteria->{$_}) {
2552 push @filters, {
2553 field => $_,
2554 value => $criteria->{$_},
2559 # Do we need to join other tables ?
2560 my %join;
2561 if (defined $criteria->{biblionumber}) {
2562 $join{items} = 1;
2565 # Build SQL query
2566 my $where = '';
2567 if (@filters) {
2568 $where = "WHERE " . join(' AND ', map { "$_->{field} = ?" } @filters);
2570 my $query = q{
2571 SELECT issues.*
2572 FROM issues
2574 if (defined $join{items}) {
2575 $query .= q{
2576 LEFT JOIN items ON (issues.itemnumber = items.itemnumber)
2579 $query .= $where;
2581 # Execute SQL query
2582 my $dbh = C4::Context->dbh;
2583 my $sth = $dbh->prepare($query);
2584 my $rv = $sth->execute(map { $_->{value} } @filters);
2586 return $rv ? $sth->fetchall_arrayref({}) : undef;
2589 =head2 GetItemIssues
2591 $issues = &GetItemIssues($itemnumber, $history);
2593 Returns patrons that have issued a book
2595 C<$itemnumber> is the itemnumber
2596 C<$history> is false if you just want the current "issuer" (if any)
2597 and true if you want issues history from old_issues also.
2599 Returns reference to an array of hashes
2601 =cut
2603 sub GetItemIssues {
2604 my ( $itemnumber, $history ) = @_;
2606 my $today = DateTime->now( time_zome => C4::Context->tz); # get today date
2607 $today->truncate( to => 'minute' );
2608 my $sql = "SELECT * FROM issues
2609 JOIN borrowers USING (borrowernumber)
2610 JOIN items USING (itemnumber)
2611 WHERE issues.itemnumber = ? ";
2612 if ($history) {
2613 $sql .= "UNION ALL
2614 SELECT * FROM old_issues
2615 LEFT JOIN borrowers USING (borrowernumber)
2616 JOIN items USING (itemnumber)
2617 WHERE old_issues.itemnumber = ? ";
2619 $sql .= "ORDER BY date_due DESC";
2620 my $sth = C4::Context->dbh->prepare($sql);
2621 if ($history) {
2622 $sth->execute($itemnumber, $itemnumber);
2623 } else {
2624 $sth->execute($itemnumber);
2626 my $results = $sth->fetchall_arrayref({});
2627 foreach (@$results) {
2628 my $date_due = dt_from_string($_->{date_due},'sql');
2629 $date_due->truncate( to => 'minute' );
2631 $_->{overdue} = (DateTime->compare($date_due, $today) == -1) ? 1 : 0;
2633 return $results;
2636 =head2 GetBiblioIssues
2638 $issues = GetBiblioIssues($biblionumber);
2640 this function get all issues from a biblionumber.
2642 Return:
2643 C<$issues> is a reference to array which each value is ref-to-hash. This ref-to-hash containts all column from
2644 tables issues and the firstname,surname & cardnumber from borrowers.
2646 =cut
2648 sub GetBiblioIssues {
2649 my $biblionumber = shift;
2650 return unless $biblionumber;
2651 my $dbh = C4::Context->dbh;
2652 my $query = "
2653 SELECT issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2654 FROM issues
2655 LEFT JOIN borrowers ON borrowers.borrowernumber = issues.borrowernumber
2656 LEFT JOIN items ON issues.itemnumber = items.itemnumber
2657 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2658 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2659 WHERE biblio.biblionumber = ?
2660 UNION ALL
2661 SELECT old_issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2662 FROM old_issues
2663 LEFT JOIN borrowers ON borrowers.borrowernumber = old_issues.borrowernumber
2664 LEFT JOIN items ON old_issues.itemnumber = items.itemnumber
2665 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2666 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2667 WHERE biblio.biblionumber = ?
2668 ORDER BY timestamp
2670 my $sth = $dbh->prepare($query);
2671 $sth->execute($biblionumber, $biblionumber);
2673 my @issues;
2674 while ( my $data = $sth->fetchrow_hashref ) {
2675 push @issues, $data;
2677 return \@issues;
2680 =head2 GetUpcomingDueIssues
2682 my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2684 =cut
2686 sub GetUpcomingDueIssues {
2687 my $params = shift;
2689 $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2690 my $dbh = C4::Context->dbh;
2692 my $statement = <<END_SQL;
2693 SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2694 FROM issues
2695 LEFT JOIN items USING (itemnumber)
2696 LEFT OUTER JOIN branches USING (branchcode)
2697 WHERE returndate is NULL
2698 HAVING days_until_due >= 0 AND days_until_due <= ?
2699 END_SQL
2701 my @bind_parameters = ( $params->{'days_in_advance'} );
2703 my $sth = $dbh->prepare( $statement );
2704 $sth->execute( @bind_parameters );
2705 my $upcoming_dues = $sth->fetchall_arrayref({});
2707 return $upcoming_dues;
2710 =head2 CanBookBeRenewed
2712 ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber[, $override_limit]);
2714 Find out whether a borrowed item may be renewed.
2716 C<$borrowernumber> is the borrower number of the patron who currently
2717 has the item on loan.
2719 C<$itemnumber> is the number of the item to renew.
2721 C<$override_limit>, if supplied with a true value, causes
2722 the limit on the number of times that the loan can be renewed
2723 (as controlled by the item type) to be ignored. Overriding also allows
2724 to renew sooner than "No renewal before" and to manually renew loans
2725 that are automatically renewed.
2727 C<$CanBookBeRenewed> returns a true value if the item may be renewed. The
2728 item must currently be on loan to the specified borrower; renewals
2729 must be allowed for the item's type; and the borrower must not have
2730 already renewed the loan. $error will contain the reason the renewal can not proceed
2732 =cut
2734 sub CanBookBeRenewed {
2735 my ( $borrowernumber, $itemnumber, $override_limit ) = @_;
2737 my $dbh = C4::Context->dbh;
2738 my $renews = 1;
2740 my $item = GetItem($itemnumber) or return ( 0, 'no_item' );
2741 my $itemissue = GetItemIssue($itemnumber) or return ( 0, 'no_checkout' );
2742 return ( 0, 'onsite_checkout' ) if $itemissue->{onsite_checkout};
2744 $borrowernumber ||= $itemissue->{borrowernumber};
2745 my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber )
2746 or return;
2748 my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2750 # This item can fill one or more unfilled reserve, can those unfilled reserves
2751 # all be filled by other available items?
2752 if ( $resfound
2753 && C4::Context->preference('AllowRenewalIfOtherItemsAvailable') )
2755 my $schema = Koha::Database->new()->schema();
2757 my $item_holds = $schema->resultset('Reserve')->search( { itemnumber => $itemnumber, found => undef } )->count();
2758 if ($item_holds) {
2759 # There is an item level hold on this item, no other item can fill the hold
2760 $resfound = 1;
2762 else {
2764 # Get all other items that could possibly fill reserves
2765 my @itemnumbers = $schema->resultset('Item')->search(
2767 biblionumber => $resrec->{biblionumber},
2768 onloan => undef,
2769 notforloan => 0,
2770 -not => { itemnumber => $itemnumber }
2772 { columns => 'itemnumber' }
2773 )->get_column('itemnumber')->all();
2775 # Get all other reserves that could have been filled by this item
2776 my @borrowernumbers;
2777 while (1) {
2778 my ( $reserve_found, $reserve, undef ) =
2779 C4::Reserves::CheckReserves( $itemnumber, undef, undef, \@borrowernumbers );
2781 if ($reserve_found) {
2782 push( @borrowernumbers, $reserve->{borrowernumber} );
2784 else {
2785 last;
2789 # If the count of the union of the lists of reservable items for each borrower
2790 # is equal or greater than the number of borrowers, we know that all reserves
2791 # can be filled with available items. We can get the union of the sets simply
2792 # by pushing all the elements onto an array and removing the duplicates.
2793 my @reservable;
2794 foreach my $b (@borrowernumbers) {
2795 my ($borr) = C4::Members::GetMemberDetails($b);
2796 foreach my $i (@itemnumbers) {
2797 my $item = GetItem($i);
2798 if ( IsAvailableForItemLevelRequest( $item, $borr )
2799 && CanItemBeReserved( $b, $i )
2800 && !IsItemOnHoldAndFound($i) )
2802 push( @reservable, $i );
2807 @reservable = uniq(@reservable);
2809 if ( @reservable >= @borrowernumbers ) {
2810 $resfound = 0;
2814 return ( 0, "on_reserve" ) if $resfound; # '' when no hold was found
2816 return ( 1, undef ) if $override_limit;
2818 my $branchcode = _GetCircControlBranch( $item, $borrower );
2819 my $issuingrule =
2820 GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode );
2822 return ( 0, "too_many" )
2823 if $issuingrule->{renewalsallowed} <= $itemissue->{renewals};
2825 my $overduesblockrenewing = C4::Context->preference('OverduesBlockRenewing');
2826 my $restrictionblockrenewing = C4::Context->preference('RestrictionBlockRenewing');
2827 my $patron = Koha::Patrons->find($borrowernumber);
2828 my $restricted = $patron->is_debarred;
2829 my $hasoverdues = $patron->has_overdues;
2831 if ( $restricted and $restrictionblockrenewing ) {
2832 return ( 0, 'restriction');
2833 } elsif ( ($hasoverdues and $overduesblockrenewing eq 'block') || ($itemissue->{overdue} and $overduesblockrenewing eq 'blockitem') ) {
2834 return ( 0, 'overdue');
2837 if ( defined $issuingrule->{norenewalbefore}
2838 and $issuingrule->{norenewalbefore} ne "" )
2841 # Calculate soonest renewal by subtracting 'No renewal before' from due date
2842 my $soonestrenewal =
2843 $itemissue->{date_due}->clone()
2844 ->subtract(
2845 $issuingrule->{lengthunit} => $issuingrule->{norenewalbefore} );
2847 # Depending on syspref reset the exact time, only check the date
2848 if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
2849 and $issuingrule->{lengthunit} eq 'days' )
2851 $soonestrenewal->truncate( to => 'day' );
2854 if ( $soonestrenewal > DateTime->now( time_zone => C4::Context->tz() ) )
2856 return ( 0, "auto_too_soon" ) if $itemissue->{auto_renew};
2857 return ( 0, "too_soon" );
2859 elsif ( $itemissue->{auto_renew} ) {
2860 return ( 0, "auto_renew" );
2864 # Fallback for automatic renewals:
2865 # If norenewalbefore is undef, don't renew before due date.
2866 elsif ( $itemissue->{auto_renew} ) {
2867 my $now = dt_from_string;
2868 return ( 0, "auto_renew" )
2869 if $now >= $itemissue->{date_due};
2870 return ( 0, "auto_too_soon" );
2873 return ( 1, undef );
2876 =head2 AddRenewal
2878 &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
2880 Renews a loan.
2882 C<$borrowernumber> is the borrower number of the patron who currently
2883 has the item.
2885 C<$itemnumber> is the number of the item to renew.
2887 C<$branch> is the library where the renewal took place (if any).
2888 The library that controls the circ policies for the renewal is retrieved from the issues record.
2890 C<$datedue> can be a DateTime object used to set the due date.
2892 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate. If
2893 this parameter is not supplied, lastreneweddate is set to the current date.
2895 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2896 from the book's item type.
2898 =cut
2900 sub AddRenewal {
2901 my $borrowernumber = shift;
2902 my $itemnumber = shift or return;
2903 my $branch = shift;
2904 my $datedue = shift;
2905 my $lastreneweddate = shift || DateTime->now(time_zone => C4::Context->tz)->ymd();
2907 my $item = GetItem($itemnumber) or return;
2908 my $biblio = GetBiblioFromItemNumber($itemnumber) or return;
2910 my $dbh = C4::Context->dbh;
2912 # Find the issues record for this book
2913 my $issuedata = GetItemIssue($itemnumber);
2915 return unless ( $issuedata );
2917 $borrowernumber ||= $issuedata->{borrowernumber};
2919 if ( defined $datedue && ref $datedue ne 'DateTime' ) {
2920 carp 'Invalid date passed to AddRenewal.';
2921 return;
2924 my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber ) or return;
2926 if ( C4::Context->preference('CalculateFinesOnReturn') && $issuedata->{overdue} ) {
2927 _CalculateAndUpdateFine( { issue => $issuedata, item => $item, borrower => $borrower } );
2929 _FixOverduesOnReturn( $borrowernumber, $itemnumber );
2931 # If the due date wasn't specified, calculate it by adding the
2932 # book's loan length to today's date or the current due date
2933 # based on the value of the RenewalPeriodBase syspref.
2934 unless ($datedue) {
2936 my $itemtype = (C4::Context->preference('item-level_itypes')) ? $biblio->{'itype'} : $biblio->{'itemtype'};
2938 $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
2939 dt_from_string( $issuedata->{date_due} ) :
2940 DateTime->now( time_zone => C4::Context->tz());
2941 $datedue = CalcDateDue($datedue, $itemtype, $issuedata->{'branchcode'}, $borrower, 'is a renewal');
2944 # Update the issues record to have the new due date, and a new count
2945 # of how many times it has been renewed.
2946 my $renews = $issuedata->{'renewals'} + 1;
2947 my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
2948 WHERE borrowernumber=?
2949 AND itemnumber=?"
2952 $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
2954 # Update the renewal count on the item, and tell zebra to reindex
2955 $renews = $biblio->{'renewals'} + 1;
2956 ModItem({ renewals => $renews, onloan => $datedue->strftime('%Y-%m-%d %H:%M')}, $biblio->{'biblionumber'}, $itemnumber);
2958 # Charge a new rental fee, if applicable?
2959 my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
2960 if ( $charge > 0 ) {
2961 my $accountno = getnextacctno( $borrowernumber );
2962 my $item = GetBiblioFromItemNumber($itemnumber);
2963 my $manager_id = 0;
2964 $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
2965 $sth = $dbh->prepare(
2966 "INSERT INTO accountlines
2967 (date, borrowernumber, accountno, amount, manager_id,
2968 description,accounttype, amountoutstanding, itemnumber)
2969 VALUES (now(),?,?,?,?,?,?,?,?)"
2971 $sth->execute( $borrowernumber, $accountno, $charge, $manager_id,
2972 "Renewal of Rental Item $item->{'title'} $item->{'barcode'}",
2973 'Rent', $charge, $itemnumber );
2976 # Send a renewal slip according to checkout alert preferencei
2977 if ( C4::Context->preference('RenewalSendNotice') eq '1' ) {
2978 $borrower = C4::Members::GetMemberDetails( $borrowernumber, 0 );
2979 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2980 my %conditions = (
2981 branchcode => $branch,
2982 categorycode => $borrower->{categorycode},
2983 item_type => $item->{itype},
2984 notification => 'CHECKOUT',
2986 if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
2987 SendCirculationAlert(
2989 type => 'RENEWAL',
2990 item => $item,
2991 borrower => $borrower,
2992 branch => $branch,
2998 # Remove any OVERDUES related debarment if the borrower has no overdues
2999 $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber );
3000 if ( $borrowernumber
3001 && $borrower->{'debarred'}
3002 && !Koha::Patrons->find( $borrowernumber )->has_overdues
3003 && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
3005 DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
3008 # Log the renewal
3009 UpdateStats({branch => $branch,
3010 type => 'renew',
3011 amount => $charge,
3012 itemnumber => $itemnumber,
3013 itemtype => $item->{itype},
3014 borrowernumber => $borrowernumber,
3015 ccode => $item->{'ccode'}}
3017 return $datedue;
3020 sub GetRenewCount {
3021 # check renewal status
3022 my ( $bornum, $itemno ) = @_;
3023 my $dbh = C4::Context->dbh;
3024 my $renewcount = 0;
3025 my $renewsallowed = 0;
3026 my $renewsleft = 0;
3028 my $borrower = C4::Members::GetMember( borrowernumber => $bornum);
3029 my $item = GetItem($itemno);
3031 # Look in the issues table for this item, lent to this borrower,
3032 # and not yet returned.
3034 # FIXME - I think this function could be redone to use only one SQL call.
3035 my $sth = $dbh->prepare(
3036 "select * from issues
3037 where (borrowernumber = ?)
3038 and (itemnumber = ?)"
3040 $sth->execute( $bornum, $itemno );
3041 my $data = $sth->fetchrow_hashref;
3042 $renewcount = $data->{'renewals'} if $data->{'renewals'};
3043 # $item and $borrower should be calculated
3044 my $branchcode = _GetCircControlBranch($item, $borrower);
3046 my $issuingrule = GetIssuingRule($borrower->{categorycode}, $item->{itype}, $branchcode);
3048 $renewsallowed = $issuingrule->{'renewalsallowed'};
3049 $renewsleft = $renewsallowed - $renewcount;
3050 if($renewsleft < 0){ $renewsleft = 0; }
3051 return ( $renewcount, $renewsallowed, $renewsleft );
3054 =head2 GetSoonestRenewDate
3056 $NoRenewalBeforeThisDate = &GetSoonestRenewDate($borrowernumber, $itemnumber);
3058 Find out the soonest possible renew date of a borrowed item.
3060 C<$borrowernumber> is the borrower number of the patron who currently
3061 has the item on loan.
3063 C<$itemnumber> is the number of the item to renew.
3065 C<$GetSoonestRenewDate> returns the DateTime of the soonest possible
3066 renew date, based on the value "No renewal before" of the applicable
3067 issuing rule. Returns the current date if the item can already be
3068 renewed, and returns undefined if the borrower, loan, or item
3069 cannot be found.
3071 =cut
3073 sub GetSoonestRenewDate {
3074 my ( $borrowernumber, $itemnumber ) = @_;
3076 my $dbh = C4::Context->dbh;
3078 my $item = GetItem($itemnumber) or return;
3079 my $itemissue = GetItemIssue($itemnumber) or return;
3081 $borrowernumber ||= $itemissue->{borrowernumber};
3082 my $borrower = C4::Members::GetMemberDetails($borrowernumber)
3083 or return;
3085 my $branchcode = _GetCircControlBranch( $item, $borrower );
3086 my $issuingrule =
3087 GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode );
3089 my $now = dt_from_string;
3091 if ( defined $issuingrule->{norenewalbefore}
3092 and $issuingrule->{norenewalbefore} ne "" )
3094 my $soonestrenewal =
3095 $itemissue->{date_due}->clone()
3096 ->subtract(
3097 $issuingrule->{lengthunit} => $issuingrule->{norenewalbefore} );
3099 if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
3100 and $issuingrule->{lengthunit} eq 'days' )
3102 $soonestrenewal->truncate( to => 'day' );
3104 return $soonestrenewal if $now < $soonestrenewal;
3106 return $now;
3109 =head2 GetIssuingCharges
3111 ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
3113 Calculate how much it would cost for a given patron to borrow a given
3114 item, including any applicable discounts.
3116 C<$itemnumber> is the item number of item the patron wishes to borrow.
3118 C<$borrowernumber> is the patron's borrower number.
3120 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
3121 and C<$item_type> is the code for the item's item type (e.g., C<VID>
3122 if it's a video).
3124 =cut
3126 sub GetIssuingCharges {
3128 # calculate charges due
3129 my ( $itemnumber, $borrowernumber ) = @_;
3130 my $charge = 0;
3131 my $dbh = C4::Context->dbh;
3132 my $item_type;
3134 # Get the book's item type and rental charge (via its biblioitem).
3135 my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
3136 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
3137 $charge_query .= (C4::Context->preference('item-level_itypes'))
3138 ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
3139 : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
3141 $charge_query .= ' WHERE items.itemnumber =?';
3143 my $sth = $dbh->prepare($charge_query);
3144 $sth->execute($itemnumber);
3145 if ( my $item_data = $sth->fetchrow_hashref ) {
3146 $item_type = $item_data->{itemtype};
3147 $charge = $item_data->{rentalcharge};
3148 my $branch = C4::Context::mybranch();
3149 my $discount_query = q|SELECT rentaldiscount,
3150 issuingrules.itemtype, issuingrules.branchcode
3151 FROM borrowers
3152 LEFT JOIN issuingrules ON borrowers.categorycode = issuingrules.categorycode
3153 WHERE borrowers.borrowernumber = ?
3154 AND (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
3155 AND (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')|;
3156 my $discount_sth = $dbh->prepare($discount_query);
3157 $discount_sth->execute( $borrowernumber, $item_type, $branch );
3158 my $discount_rules = $discount_sth->fetchall_arrayref({});
3159 if (@{$discount_rules}) {
3160 # We may have multiple rules so get the most specific
3161 my $discount = _get_discount_from_rule($discount_rules, $branch, $item_type);
3162 $charge = ( $charge * ( 100 - $discount ) ) / 100;
3166 return ( $charge, $item_type );
3169 # Select most appropriate discount rule from those returned
3170 sub _get_discount_from_rule {
3171 my ($rules_ref, $branch, $itemtype) = @_;
3172 my $discount;
3174 if (@{$rules_ref} == 1) { # only 1 applicable rule use it
3175 $discount = $rules_ref->[0]->{rentaldiscount};
3176 return (defined $discount) ? $discount : 0;
3178 # could have up to 4 does one match $branch and $itemtype
3179 my @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq $itemtype } @{$rules_ref};
3180 if (@d) {
3181 $discount = $d[0]->{rentaldiscount};
3182 return (defined $discount) ? $discount : 0;
3184 # do we have item type + all branches
3185 @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq $itemtype } @{$rules_ref};
3186 if (@d) {
3187 $discount = $d[0]->{rentaldiscount};
3188 return (defined $discount) ? $discount : 0;
3190 # do we all item types + this branch
3191 @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq q{*} } @{$rules_ref};
3192 if (@d) {
3193 $discount = $d[0]->{rentaldiscount};
3194 return (defined $discount) ? $discount : 0;
3196 # so all and all (surely we wont get here)
3197 @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq q{*} } @{$rules_ref};
3198 if (@d) {
3199 $discount = $d[0]->{rentaldiscount};
3200 return (defined $discount) ? $discount : 0;
3202 # none of the above
3203 return 0;
3206 =head2 AddIssuingCharge
3208 &AddIssuingCharge( $itemno, $borrowernumber, $charge )
3210 =cut
3212 sub AddIssuingCharge {
3213 my ( $itemnumber, $borrowernumber, $charge ) = @_;
3214 my $dbh = C4::Context->dbh;
3215 my $nextaccntno = getnextacctno( $borrowernumber );
3216 my $manager_id = 0;
3217 $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
3218 my $query ="
3219 INSERT INTO accountlines
3220 (borrowernumber, itemnumber, accountno,
3221 date, amount, description, accounttype,
3222 amountoutstanding, manager_id)
3223 VALUES (?, ?, ?,now(), ?, 'Rental', 'Rent',?,?)
3225 my $sth = $dbh->prepare($query);
3226 $sth->execute( $borrowernumber, $itemnumber, $nextaccntno, $charge, $charge, $manager_id );
3229 =head2 GetTransfers
3231 GetTransfers($itemnumber);
3233 =cut
3235 sub GetTransfers {
3236 my ($itemnumber) = @_;
3238 my $dbh = C4::Context->dbh;
3240 my $query = '
3241 SELECT datesent,
3242 frombranch,
3243 tobranch
3244 FROM branchtransfers
3245 WHERE itemnumber = ?
3246 AND datearrived IS NULL
3248 my $sth = $dbh->prepare($query);
3249 $sth->execute($itemnumber);
3250 my @row = $sth->fetchrow_array();
3251 return @row;
3254 =head2 GetTransfersFromTo
3256 @results = GetTransfersFromTo($frombranch,$tobranch);
3258 Returns the list of pending transfers between $from and $to branch
3260 =cut
3262 sub GetTransfersFromTo {
3263 my ( $frombranch, $tobranch ) = @_;
3264 return unless ( $frombranch && $tobranch );
3265 my $dbh = C4::Context->dbh;
3266 my $query = "
3267 SELECT itemnumber,datesent,frombranch
3268 FROM branchtransfers
3269 WHERE frombranch=?
3270 AND tobranch=?
3271 AND datearrived IS NULL
3273 my $sth = $dbh->prepare($query);
3274 $sth->execute( $frombranch, $tobranch );
3275 my @gettransfers;
3277 while ( my $data = $sth->fetchrow_hashref ) {
3278 push @gettransfers, $data;
3280 return (@gettransfers);
3283 =head2 DeleteTransfer
3285 &DeleteTransfer($itemnumber);
3287 =cut
3289 sub DeleteTransfer {
3290 my ($itemnumber) = @_;
3291 return unless $itemnumber;
3292 my $dbh = C4::Context->dbh;
3293 my $sth = $dbh->prepare(
3294 "DELETE FROM branchtransfers
3295 WHERE itemnumber=?
3296 AND datearrived IS NULL "
3298 return $sth->execute($itemnumber);
3301 =head2 AnonymiseIssueHistory
3303 ($rows,$err_history_not_deleted) = AnonymiseIssueHistory($date,$borrowernumber)
3305 This function write NULL instead of C<$borrowernumber> given on input arg into the table issues.
3306 if C<$borrowernumber> is not set, it will delete the issue history for all borrower older than C<$date>.
3308 If c<$borrowernumber> is set, it will delete issue history for only that borrower, regardless of their opac privacy
3309 setting (force delete).
3311 return the number of affected rows and a value that evaluates to true if an error occurred deleting the history.
3313 =cut
3315 sub AnonymiseIssueHistory {
3316 my $date = shift;
3317 my $borrowernumber = shift;
3318 my $dbh = C4::Context->dbh;
3319 my $query = "
3320 UPDATE old_issues
3321 SET borrowernumber = ?
3322 WHERE returndate < ?
3323 AND borrowernumber IS NOT NULL
3326 # The default of 0 does not work due to foreign key constraints
3327 # The anonymisation should not fail quietly if AnonymousPatron is not a valid entry
3328 # Set it to undef (NULL)
3329 my $anonymouspatron = C4::Context->preference('AnonymousPatron') || undef;
3330 my @bind_params = ($anonymouspatron, $date);
3331 if (defined $borrowernumber) {
3332 $query .= " AND borrowernumber = ?";
3333 push @bind_params, $borrowernumber;
3334 } else {
3335 $query .= " AND (SELECT privacy FROM borrowers WHERE borrowers.borrowernumber=old_issues.borrowernumber) <> 0";
3337 my $sth = $dbh->prepare($query);
3338 $sth->execute(@bind_params);
3339 my $anonymisation_err = $dbh->err;
3340 my $rows_affected = $sth->rows; ### doublecheck row count return function
3341 return ($rows_affected, $anonymisation_err);
3344 =head2 SendCirculationAlert
3346 Send out a C<check-in> or C<checkout> alert using the messaging system.
3348 B<Parameters>:
3350 =over 4
3352 =item type
3354 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
3356 =item item
3358 Hashref of information about the item being checked in or out.
3360 =item borrower
3362 Hashref of information about the borrower of the item.
3364 =item branch
3366 The branchcode from where the checkout or check-in took place.
3368 =back
3370 B<Example>:
3372 SendCirculationAlert({
3373 type => 'CHECKOUT',
3374 item => $item,
3375 borrower => $borrower,
3376 branch => $branch,
3379 =cut
3381 sub SendCirculationAlert {
3382 my ($opts) = @_;
3383 my ($type, $item, $borrower, $branch) =
3384 ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
3385 my %message_name = (
3386 CHECKIN => 'Item_Check_in',
3387 CHECKOUT => 'Item_Checkout',
3388 RENEWAL => 'Item_Checkout',
3390 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
3391 borrowernumber => $borrower->{borrowernumber},
3392 message_name => $message_name{$type},
3394 my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues';
3396 my @transports = keys %{ $borrower_preferences->{transports} };
3397 # warn "no transports" unless @transports;
3398 for (@transports) {
3399 # warn "transport: $_";
3400 my $message = C4::Message->find_last_message($borrower, $type, $_);
3401 if (!$message) {
3402 #warn "create new message";
3403 my $letter = C4::Letters::GetPreparedLetter (
3404 module => 'circulation',
3405 letter_code => $type,
3406 branchcode => $branch,
3407 message_transport_type => $_,
3408 tables => {
3409 $issues_table => $item->{itemnumber},
3410 'items' => $item->{itemnumber},
3411 'biblio' => $item->{biblionumber},
3412 'biblioitems' => $item->{biblionumber},
3413 'borrowers' => $borrower,
3414 'branches' => $branch,
3416 ) or next;
3417 C4::Message->enqueue($letter, $borrower, $_);
3418 } else {
3419 #warn "append to old message";
3420 my $letter = C4::Letters::GetPreparedLetter (
3421 module => 'circulation',
3422 letter_code => $type,
3423 branchcode => $branch,
3424 message_transport_type => $_,
3425 tables => {
3426 $issues_table => $item->{itemnumber},
3427 'items' => $item->{itemnumber},
3428 'biblio' => $item->{biblionumber},
3429 'biblioitems' => $item->{biblionumber},
3430 'borrowers' => $borrower,
3431 'branches' => $branch,
3433 ) or next;
3434 $message->append($letter);
3435 $message->update;
3439 return;
3442 =head2 updateWrongTransfer
3444 $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
3446 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
3448 =cut
3450 sub updateWrongTransfer {
3451 my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
3452 my $dbh = C4::Context->dbh;
3453 # first step validate the actual line of transfert .
3454 my $sth =
3455 $dbh->prepare(
3456 "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
3458 $sth->execute($FromLibrary,$itemNumber);
3460 # second step create a new line of branchtransfer to the right location .
3461 ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
3463 #third step changing holdingbranch of item
3464 UpdateHoldingbranch($FromLibrary,$itemNumber);
3467 =head2 UpdateHoldingbranch
3469 $items = UpdateHoldingbranch($branch,$itmenumber);
3471 Simple methode for updating hodlingbranch in items BDD line
3473 =cut
3475 sub UpdateHoldingbranch {
3476 my ( $branch,$itemnumber ) = @_;
3477 ModItem({ holdingbranch => $branch }, undef, $itemnumber);
3480 =head2 CalcDateDue
3482 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
3484 this function calculates the due date given the start date and configured circulation rules,
3485 checking against the holidays calendar as per the 'useDaysMode' syspref.
3486 C<$startdate> = DateTime object representing start date of loan period (assumed to be today)
3487 C<$itemtype> = itemtype code of item in question
3488 C<$branch> = location whose calendar to use
3489 C<$borrower> = Borrower object
3490 C<$isrenewal> = Boolean: is true if we want to calculate the date due for a renewal. Else is false.
3492 =cut
3494 sub CalcDateDue {
3495 my ( $startdate, $itemtype, $branch, $borrower, $isrenewal ) = @_;
3497 $isrenewal ||= 0;
3499 # loanlength now a href
3500 my $loanlength =
3501 GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
3503 my $length_key = ( $isrenewal and defined $loanlength->{renewalperiod} )
3504 ? qq{renewalperiod}
3505 : qq{issuelength};
3507 my $datedue;
3508 if ( $startdate ) {
3509 if (ref $startdate ne 'DateTime' ) {
3510 $datedue = dt_from_string($datedue);
3511 } else {
3512 $datedue = $startdate->clone;
3514 } else {
3515 $datedue =
3516 DateTime->now( time_zone => C4::Context->tz() )
3517 ->truncate( to => 'minute' );
3521 # calculate the datedue as normal
3522 if ( C4::Context->preference('useDaysMode') eq 'Days' )
3523 { # ignoring calendar
3524 if ( $loanlength->{lengthunit} eq 'hours' ) {
3525 $datedue->add( hours => $loanlength->{$length_key} );
3526 } else { # days
3527 $datedue->add( days => $loanlength->{$length_key} );
3528 $datedue->set_hour(23);
3529 $datedue->set_minute(59);
3531 } else {
3532 my $dur;
3533 if ($loanlength->{lengthunit} eq 'hours') {
3534 $dur = DateTime::Duration->new( hours => $loanlength->{$length_key});
3536 else { # days
3537 $dur = DateTime::Duration->new( days => $loanlength->{$length_key});
3539 my $calendar = Koha::Calendar->new( branchcode => $branch );
3540 $datedue = $calendar->addDate( $datedue, $dur, $loanlength->{lengthunit} );
3541 if ($loanlength->{lengthunit} eq 'days') {
3542 $datedue->set_hour(23);
3543 $datedue->set_minute(59);
3547 # if Hard Due Dates are used, retrieve them and apply as necessary
3548 my ( $hardduedate, $hardduedatecompare ) =
3549 GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
3550 if ($hardduedate) { # hardduedates are currently dates
3551 $hardduedate->truncate( to => 'minute' );
3552 $hardduedate->set_hour(23);
3553 $hardduedate->set_minute(59);
3554 my $cmp = DateTime->compare( $hardduedate, $datedue );
3556 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
3557 # if the calculated date is before the 'after' Hard Due Date (floor), override
3558 # if the hard due date is set to 'exactly', overrride
3559 if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
3560 $datedue = $hardduedate->clone;
3563 # in all other cases, keep the date due as it is
3567 # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3568 if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3569 my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso', 'floating');
3570 if( $expiry_dt ) { #skip empty expiry date..
3571 $expiry_dt->set( hour => 23, minute => 59);
3572 my $d1= $datedue->clone->set_time_zone('floating');
3573 if ( DateTime->compare( $d1, $expiry_dt ) == 1 ) {
3574 $datedue = $expiry_dt->clone->set_time_zone( C4::Context->tz );
3579 return $datedue;
3583 sub CheckValidBarcode{
3584 my ($barcode) = @_;
3585 my $dbh = C4::Context->dbh;
3586 my $query=qq|SELECT count(*)
3587 FROM items
3588 WHERE barcode=?
3590 my $sth = $dbh->prepare($query);
3591 $sth->execute($barcode);
3592 my $exist=$sth->fetchrow ;
3593 return $exist;
3596 =head2 IsBranchTransferAllowed
3598 $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3600 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3602 =cut
3604 sub IsBranchTransferAllowed {
3605 my ( $toBranch, $fromBranch, $code ) = @_;
3607 if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3609 my $limitType = C4::Context->preference("BranchTransferLimitsType");
3610 my $dbh = C4::Context->dbh;
3612 my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3613 $sth->execute( $toBranch, $fromBranch, $code );
3614 my $limit = $sth->fetchrow_hashref();
3616 ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3617 if ( $limit->{'limitId'} ) {
3618 return 0;
3619 } else {
3620 return 1;
3624 =head2 CreateBranchTransferLimit
3626 CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3628 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3630 =cut
3632 sub CreateBranchTransferLimit {
3633 my ( $toBranch, $fromBranch, $code ) = @_;
3634 return unless defined($toBranch) && defined($fromBranch);
3635 my $limitType = C4::Context->preference("BranchTransferLimitsType");
3637 my $dbh = C4::Context->dbh;
3639 my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3640 return $sth->execute( $code, $toBranch, $fromBranch );
3643 =head2 DeleteBranchTransferLimits
3645 my $result = DeleteBranchTransferLimits($frombranch);
3647 Deletes all the library transfer limits for one library. Returns the
3648 number of limits deleted, 0e0 if no limits were deleted, or undef if
3649 no arguments are supplied.
3651 =cut
3653 sub DeleteBranchTransferLimits {
3654 my $branch = shift;
3655 return unless defined $branch;
3656 my $dbh = C4::Context->dbh;
3657 my $sth = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3658 return $sth->execute($branch);
3661 sub ReturnLostItem{
3662 my ( $borrowernumber, $itemnum ) = @_;
3664 MarkIssueReturned( $borrowernumber, $itemnum );
3665 my $borrower = C4::Members::GetMember( 'borrowernumber'=>$borrowernumber );
3666 my $item = C4::Items::GetItem( $itemnum );
3667 my $old_note = ($item->{'paidfor'} && ($item->{'paidfor'} ne q{})) ? $item->{'paidfor'}.' / ' : q{};
3668 my @datearr = localtime(time);
3669 my $date = ( 1900 + $datearr[5] ) . "-" . ( $datearr[4] + 1 ) . "-" . $datearr[3];
3670 my $bor = "$borrower->{'firstname'} $borrower->{'surname'} $borrower->{'cardnumber'}";
3671 ModItem({ paidfor => $old_note."Paid for by $bor $date" }, undef, $itemnum);
3675 sub LostItem{
3676 my ($itemnumber, $mark_returned) = @_;
3678 my $dbh = C4::Context->dbh();
3679 my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title
3680 FROM issues
3681 JOIN items USING (itemnumber)
3682 JOIN biblio USING (biblionumber)
3683 WHERE issues.itemnumber=?");
3684 $sth->execute($itemnumber);
3685 my $issues=$sth->fetchrow_hashref();
3687 # If a borrower lost the item, add a replacement cost to the their record
3688 if ( my $borrowernumber = $issues->{borrowernumber} ){
3689 my $borrower = C4::Members::GetMemberDetails( $borrowernumber );
3691 if (C4::Context->preference('WhenLostForgiveFine')){
3692 my $fix = _FixOverduesOnReturn($borrowernumber, $itemnumber, 1, 0); # 1, 0 = exemptfine, no-dropbox
3693 defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $itemnumber...) failed!"; # zero is OK, check defined
3695 if (C4::Context->preference('WhenLostChargeReplacementFee')){
3696 C4::Accounts::chargelostitem($borrowernumber, $itemnumber, $issues->{'replacementprice'}, "Lost Item $issues->{'title'} $issues->{'barcode'}");
3697 #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3698 #warn " $issues->{'borrowernumber'} / $itemnumber ";
3701 MarkIssueReturned($borrowernumber,$itemnumber,undef,undef,$borrower->{'privacy'}) if $mark_returned;
3705 sub GetOfflineOperations {
3706 my $dbh = C4::Context->dbh;
3707 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3708 $sth->execute(C4::Context->userenv->{'branch'});
3709 my $results = $sth->fetchall_arrayref({});
3710 return $results;
3713 sub GetOfflineOperation {
3714 my $operationid = shift;
3715 return unless $operationid;
3716 my $dbh = C4::Context->dbh;
3717 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3718 $sth->execute( $operationid );
3719 return $sth->fetchrow_hashref;
3722 sub AddOfflineOperation {
3723 my ( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount ) = @_;
3724 my $dbh = C4::Context->dbh;
3725 my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber, amount) VALUES(?,?,?,?,?,?,?)");
3726 $sth->execute( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount );
3727 return "Added.";
3730 sub DeleteOfflineOperation {
3731 my $dbh = C4::Context->dbh;
3732 my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3733 $sth->execute( shift );
3734 return "Deleted.";
3737 sub ProcessOfflineOperation {
3738 my $operation = shift;
3740 my $report;
3741 if ( $operation->{action} eq 'return' ) {
3742 $report = ProcessOfflineReturn( $operation );
3743 } elsif ( $operation->{action} eq 'issue' ) {
3744 $report = ProcessOfflineIssue( $operation );
3745 } elsif ( $operation->{action} eq 'payment' ) {
3746 $report = ProcessOfflinePayment( $operation );
3749 DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3751 return $report;
3754 sub ProcessOfflineReturn {
3755 my $operation = shift;
3757 my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3759 if ( $itemnumber ) {
3760 my $issue = GetOpenIssue( $itemnumber );
3761 if ( $issue ) {
3762 MarkIssueReturned(
3763 $issue->{borrowernumber},
3764 $itemnumber,
3765 undef,
3766 $operation->{timestamp},
3768 ModItem(
3769 { renewals => 0, onloan => undef },
3770 $issue->{'biblionumber'},
3771 $itemnumber
3773 return "Success.";
3774 } else {
3775 return "Item not issued.";
3777 } else {
3778 return "Item not found.";
3782 sub ProcessOfflineIssue {
3783 my $operation = shift;
3785 my $borrower = C4::Members::GetMemberDetails( undef, $operation->{cardnumber} ); # Get borrower from operation cardnumber
3787 if ( $borrower->{borrowernumber} ) {
3788 my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3789 unless ($itemnumber) {
3790 return "Barcode not found.";
3792 my $issue = GetOpenIssue( $itemnumber );
3794 if ( $issue and ( $issue->{borrowernumber} ne $borrower->{borrowernumber} ) ) { # Item already issued to another borrower, mark it returned
3795 MarkIssueReturned(
3796 $issue->{borrowernumber},
3797 $itemnumber,
3798 undef,
3799 $operation->{timestamp},
3802 AddIssue(
3803 $borrower,
3804 $operation->{'barcode'},
3805 undef,
3807 $operation->{timestamp},
3808 undef,
3810 return "Success.";
3811 } else {
3812 return "Borrower not found.";
3816 sub ProcessOfflinePayment {
3817 my $operation = shift;
3819 my $borrower = C4::Members::GetMemberDetails( undef, $operation->{cardnumber} ); # Get borrower from operation cardnumber
3820 my $amount = $operation->{amount};
3822 recordpayment( $borrower->{borrowernumber}, $amount );
3824 return "Success."
3828 =head2 TransferSlip
3830 TransferSlip($user_branch, $itemnumber, $barcode, $to_branch)
3832 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
3834 =cut
3836 sub TransferSlip {
3837 my ($branch, $itemnumber, $barcode, $to_branch) = @_;
3839 my $item = GetItem( $itemnumber, $barcode )
3840 or return;
3842 return C4::Letters::GetPreparedLetter (
3843 module => 'circulation',
3844 letter_code => 'TRANSFERSLIP',
3845 branchcode => $branch,
3846 tables => {
3847 'branches' => $to_branch,
3848 'biblio' => $item->{biblionumber},
3849 'items' => $item,
3854 =head2 CheckIfIssuedToPatron
3856 CheckIfIssuedToPatron($borrowernumber, $biblionumber)
3858 Return 1 if any record item is issued to patron, otherwise return 0
3860 =cut
3862 sub CheckIfIssuedToPatron {
3863 my ($borrowernumber, $biblionumber) = @_;
3865 my $dbh = C4::Context->dbh;
3866 my $query = q|
3867 SELECT COUNT(*) FROM issues
3868 LEFT JOIN items ON items.itemnumber = issues.itemnumber
3869 WHERE items.biblionumber = ?
3870 AND issues.borrowernumber = ?
3872 my $is_issued = $dbh->selectrow_array($query, {}, $biblionumber, $borrowernumber );
3873 return 1 if $is_issued;
3874 return;
3877 =head2 IsItemIssued
3879 IsItemIssued( $itemnumber )
3881 Return 1 if the item is on loan, otherwise return 0
3883 =cut
3885 sub IsItemIssued {
3886 my $itemnumber = shift;
3887 my $dbh = C4::Context->dbh;
3888 my $sth = $dbh->prepare(q{
3889 SELECT COUNT(*)
3890 FROM issues
3891 WHERE itemnumber = ?
3893 $sth->execute($itemnumber);
3894 return $sth->fetchrow;
3897 =head2 GetAgeRestriction
3899 my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions, $borrower);
3900 my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions);
3902 if($daysToAgeRestriction <= 0) { #Borrower is allowed to access this material, as he is older or as old as the agerestriction }
3903 if($daysToAgeRestriction > 0) { #Borrower is this many days from meeting the agerestriction }
3905 @PARAM1 the koha.biblioitems.agerestriction value, like K18, PEGI 13, ...
3906 @PARAM2 a borrower-object with koha.borrowers.dateofbirth. (OPTIONAL)
3907 @RETURNS The age restriction age in years and the days to fulfill the age restriction for the given borrower.
3908 Negative days mean the borrower has gone past the age restriction age.
3910 =cut
3912 sub GetAgeRestriction {
3913 my ($record_restrictions, $borrower) = @_;
3914 my $markers = C4::Context->preference('AgeRestrictionMarker');
3916 # Split $record_restrictions to something like FSK 16 or PEGI 6
3917 my @values = split ' ', uc($record_restrictions);
3918 return unless @values;
3920 # Search first occurrence of one of the markers
3921 my @markers = split /\|/, uc($markers);
3922 return unless @markers;
3924 my $index = 0;
3925 my $restriction_year = 0;
3926 for my $value (@values) {
3927 $index++;
3928 for my $marker (@markers) {
3929 $marker =~ s/^\s+//; #remove leading spaces
3930 $marker =~ s/\s+$//; #remove trailing spaces
3931 if ( $marker eq $value ) {
3932 if ( $index <= $#values ) {
3933 $restriction_year += $values[$index];
3935 last;
3937 elsif ( $value =~ /^\Q$marker\E(\d+)$/ ) {
3939 # Perhaps it is something like "K16" (as in Finland)
3940 $restriction_year += $1;
3941 last;
3944 last if ( $restriction_year > 0 );
3947 #Check if the borrower is age restricted for this material and for how long.
3948 if ($restriction_year && $borrower) {
3949 if ( $borrower->{'dateofbirth'} ) {
3950 my @alloweddate = split /-/, $borrower->{'dateofbirth'};
3951 $alloweddate[0] += $restriction_year;
3953 #Prevent runime eror on leap year (invalid date)
3954 if ( ( $alloweddate[1] == 2 ) && ( $alloweddate[2] == 29 ) ) {
3955 $alloweddate[2] = 28;
3958 #Get how many days the borrower has to reach the age restriction
3959 my @Today = split /-/, DateTime->today->ymd();
3960 my $daysToAgeRestriction = Date_to_Days(@alloweddate) - Date_to_Days(@Today);
3961 #Negative days means the borrower went past the age restriction age
3962 return ($restriction_year, $daysToAgeRestriction);
3966 return ($restriction_year);
3970 =head2 GetPendingOnSiteCheckouts
3972 =cut
3974 sub GetPendingOnSiteCheckouts {
3975 my $dbh = C4::Context->dbh;
3976 return $dbh->selectall_arrayref(q|
3977 SELECT
3978 items.barcode,
3979 items.biblionumber,
3980 items.itemnumber,
3981 items.itemnotes,
3982 items.itemcallnumber,
3983 items.location,
3984 issues.date_due,
3985 issues.branchcode,
3986 issues.date_due < NOW() AS is_overdue,
3987 biblio.author,
3988 biblio.title,
3989 borrowers.firstname,
3990 borrowers.surname,
3991 borrowers.cardnumber,
3992 borrowers.borrowernumber
3993 FROM items
3994 LEFT JOIN issues ON items.itemnumber = issues.itemnumber
3995 LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
3996 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
3997 WHERE issues.onsite_checkout = 1
3998 |, { Slice => {} } );
4001 sub GetTopIssues {
4002 my ($params) = @_;
4004 my ($count, $branch, $itemtype, $ccode, $newness)
4005 = @$params{qw(count branch itemtype ccode newness)};
4007 my $dbh = C4::Context->dbh;
4008 my $query = q{
4009 SELECT b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
4010 bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
4011 i.ccode, SUM(i.issues) AS count
4012 FROM biblio b
4013 LEFT JOIN items i ON (i.biblionumber = b.biblionumber)
4014 LEFT JOIN biblioitems bi ON (bi.biblionumber = b.biblionumber)
4017 my (@where_strs, @where_args);
4019 if ($branch) {
4020 push @where_strs, 'i.homebranch = ?';
4021 push @where_args, $branch;
4023 if ($itemtype) {
4024 if (C4::Context->preference('item-level_itypes')){
4025 push @where_strs, 'i.itype = ?';
4026 push @where_args, $itemtype;
4027 } else {
4028 push @where_strs, 'bi.itemtype = ?';
4029 push @where_args, $itemtype;
4032 if ($ccode) {
4033 push @where_strs, 'i.ccode = ?';
4034 push @where_args, $ccode;
4036 if ($newness) {
4037 push @where_strs, 'TO_DAYS(NOW()) - TO_DAYS(b.datecreated) <= ?';
4038 push @where_args, $newness;
4041 if (@where_strs) {
4042 $query .= 'WHERE ' . join(' AND ', @where_strs);
4045 $query .= q{
4046 GROUP BY b.biblionumber
4047 HAVING count > 0
4048 ORDER BY count DESC
4051 $count = int($count);
4052 if ($count > 0) {
4053 $query .= "LIMIT $count";
4056 my $rows = $dbh->selectall_arrayref($query, { Slice => {} }, @where_args);
4058 return @$rows;
4061 sub _CalculateAndUpdateFine {
4062 my ($params) = @_;
4064 my $borrower = $params->{borrower};
4065 my $item = $params->{item};
4066 my $issue = $params->{issue};
4067 my $return_date = $params->{return_date};
4069 unless ($borrower) { carp "No borrower passed in!" && return; }
4070 unless ($item) { carp "No item passed in!" && return; }
4071 unless ($issue) { carp "No issue passed in!" && return; }
4073 my $datedue = $issue->{date_due};
4075 # we only need to calculate and change the fines if we want to do that on return
4076 # Should be on for hourly loans
4077 my $control = C4::Context->preference('CircControl');
4078 my $control_branchcode =
4079 ( $control eq 'ItemHomeLibrary' ) ? $item->{homebranch}
4080 : ( $control eq 'PatronLibrary' ) ? $borrower->{branchcode}
4081 : $issue->{branchcode};
4083 my $date_returned = $return_date ? dt_from_string($return_date) : dt_from_string();
4085 my ( $amount, $type, $unitcounttotal ) =
4086 C4::Overdues::CalcFine( $item, $borrower->{categorycode}, $control_branchcode, $datedue, $date_returned );
4088 $type ||= q{};
4090 if ( C4::Context->preference('finesMode') eq 'production' ) {
4091 if ( $amount > 0 ) {
4092 C4::Overdues::UpdateFine({
4093 issue_id => $issue->{issue_id},
4094 itemnumber => $issue->{itemnumber},
4095 borrowernumber => $issue->{borrowernumber},
4096 amount => $amount,
4097 type => $type,
4098 due => output_pref($datedue),
4101 elsif ($return_date) {
4103 # Backdated returns may have fines that shouldn't exist,
4104 # so in this case, we need to drop those fines to 0
4106 C4::Overdues::UpdateFine({
4107 issue_id => $issue->{issue_id},
4108 itemnumber => $issue->{itemnumber},
4109 borrowernumber => $issue->{borrowernumber},
4110 amount => 0,
4111 type => $type,
4112 due => output_pref($datedue),
4120 __END__
4122 =head1 AUTHOR
4124 Koha Development Team <http://koha-community.org/>
4126 =cut