Bug 12001: Format DEBT correctly
[koha-equinox.git] / C4 / Circulation.pm
1 package C4::Circulation;
2
3 # Copyright 2000-2002 Katipo Communications
4 # copyright 2010 BibLibre
5 #
6 # This file is part of Koha.
7 #
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.
12 #
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.
17 #
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>.
20
21
22 use strict;
23 #use warnings; FIXME - Bug 2505
24 use DateTime;
25 use Koha::DateUtils;
26 use C4::Context;
27 use C4::Stats;
28 use C4::Reserves;
29 use C4::Biblio;
30 use C4::Items;
31 use C4::Members;
32 use C4::Accounts;
33 use C4::ItemCirculationAlertPreference;
34 use C4::Message;
35 use C4::Debug;
36 use C4::Log; # logaction
37 use C4::Overdues qw(CalcFine UpdateFine get_chargeable_units);
38 use C4::RotatingCollections qw(GetCollectionItemBranches);
39 use Algorithm::CheckDigits;
40
41 use Data::Dumper;
42 use Koha::Account;
43 use Koha::AuthorisedValues;
44 use Koha::Biblioitems;
45 use Koha::DateUtils;
46 use Koha::Calendar;
47 use Koha::Checkouts;
48 use Koha::IssuingRules;
49 use Koha::Items;
50 use Koha::Patrons;
51 use Koha::Patron::Debarments;
52 use Koha::Database;
53 use Koha::Libraries;
54 use Koha::Holds;
55 use Koha::RefundLostItemFeeRule;
56 use Koha::RefundLostItemFeeRules;
57 use Koha::Account::Lines;
58 use Koha::Account::Offsets;
59 use Carp;
60 use List::MoreUtils qw( uniq );
61 use Scalar::Util qw( looks_like_number );
62 use Date::Calc qw(
63   Today
64   Today_and_Now
65   Add_Delta_YM
66   Add_Delta_DHMS
67   Date_to_Days
68   Day_of_Week
69   Add_Delta_Days
70 );
71 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
72
73 BEGIN {
74         require Exporter;
75         @ISA    = qw(Exporter);
76
77         # FIXME subs that should probably be elsewhere
78         push @EXPORT, qw(
79                 &barcodedecode
80         &LostItem
81         &ReturnLostItem
82         &GetPendingOnSiteCheckouts
83         );
84
85         # subs to deal with issuing a book
86         push @EXPORT, qw(
87                 &CanBookBeIssued
88                 &CanBookBeRenewed
89                 &AddIssue
90                 &AddRenewal
91                 &GetRenewCount
92         &GetSoonestRenewDate
93         &GetLatestAutoRenewDate
94                 &GetIssuingCharges
95         &GetBranchBorrowerCircRule
96         &GetBranchItemRule
97                 &GetBiblioIssues
98                 &GetOpenIssue
99         &CheckIfIssuedToPatron
100         &IsItemIssued
101         GetTopIssues
102         );
103
104         # subs to deal with returns
105         push @EXPORT, qw(
106                 &AddReturn
107         &MarkIssueReturned
108         );
109
110         # subs to deal with transfers
111         push @EXPORT, qw(
112                 &transferbook
113                 &GetTransfers
114                 &GetTransfersFromTo
115                 &updateWrongTransfer
116                 &DeleteTransfer
117                 &IsBranchTransferAllowed
118                 &CreateBranchTransferLimit
119                 &DeleteBranchTransferLimits
120         &TransferSlip
121         );
122
123     # subs to deal with offline circulation
124     push @EXPORT, qw(
125       &GetOfflineOperations
126       &GetOfflineOperation
127       &AddOfflineOperation
128       &DeleteOfflineOperation
129       &ProcessOfflineOperation
130     );
131 }
132
133 =head1 NAME
134
135 C4::Circulation - Koha circulation module
136
137 =head1 SYNOPSIS
138
139 use C4::Circulation;
140
141 =head1 DESCRIPTION
142
143 The functions in this module deal with circulation, issues, and
144 returns, as well as general information about the library.
145 Also deals with inventory.
146
147 =head1 FUNCTIONS
148
149 =head2 barcodedecode
150
151   $str = &barcodedecode($barcode, [$filter]);
152
153 Generic filter function for barcode string.
154 Called on every circ if the System Pref itemBarcodeInputFilter is set.
155 Will do some manipulation of the barcode for systems that deliver a barcode
156 to circulation.pl that differs from the barcode stored for the item.
157 For proper functioning of this filter, calling the function on the 
158 correct barcode string (items.barcode) should return an unaltered barcode.
159
160 The optional $filter argument is to allow for testing or explicit 
161 behavior that ignores the System Pref.  Valid values are the same as the 
162 System Pref options.
163
164 =cut
165
166 # FIXME -- the &decode fcn below should be wrapped into this one.
167 # FIXME -- these plugins should be moved out of Circulation.pm
168 #
169 sub barcodedecode {
170     my ($barcode, $filter) = @_;
171     my $branch = C4::Context::mybranch();
172     $filter = C4::Context->preference('itemBarcodeInputFilter') unless $filter;
173     $filter or return $barcode;     # ensure filter is defined, else return untouched barcode
174         if ($filter eq 'whitespace') {
175                 $barcode =~ s/\s//g;
176         } elsif ($filter eq 'cuecat') {
177                 chomp($barcode);
178             my @fields = split( /\./, $barcode );
179             my @results = map( decode($_), @fields[ 1 .. $#fields ] );
180             ($#results == 2) and return $results[2];
181         } elsif ($filter eq 'T-prefix') {
182                 if ($barcode =~ /^[Tt](\d)/) {
183                         (defined($1) and $1 eq '0') and return $barcode;
184             $barcode = substr($barcode, 2) + 0;     # FIXME: probably should be substr($barcode, 1)
185                 }
186         return sprintf("T%07d", $barcode);
187         # FIXME: $barcode could be "T1", causing warning: substr outside of string
188         # Why drop the nonzero digit after the T?
189         # Why pass non-digits (or empty string) to "T%07d"?
190         } elsif ($filter eq 'libsuite8') {
191                 unless($barcode =~ m/^($branch)-/i){    #if barcode starts with branch code its in Koha style. Skip it.
192                         if($barcode =~ m/^(\d)/i){      #Some barcodes even start with 0's & numbers and are assumed to have b as the item type in the libsuite8 software
193                                 $barcode =~ s/^[0]*(\d+)$/$branch-b-$1/i;
194                         }else{
195                                 $barcode =~ s/^(\D+)[0]*(\d+)$/$branch-$1-$2/i;
196                         }
197                 }
198     } elsif ($filter eq 'EAN13') {
199         my $ean = CheckDigits('ean');
200         if ( $ean->is_valid($barcode) ) {
201             #$barcode = sprintf('%013d',$barcode); # this doesn't work on 32-bit systems
202             $barcode = '0' x ( 13 - length($barcode) ) . $barcode;
203         } else {
204             warn "# [$barcode] not valid EAN-13/UPC-A\n";
205         }
206         }
207     return $barcode;    # return barcode, modified or not
208 }
209
210 =head2 decode
211
212   $str = &decode($chunk);
213
214 Decodes a segment of a string emitted by a CueCat barcode scanner and
215 returns it.
216
217 FIXME: Should be replaced with Barcode::Cuecat from CPAN
218 or Javascript based decoding on the client side.
219
220 =cut
221
222 sub decode {
223     my ($encoded) = @_;
224     my $seq =
225       'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-';
226     my @s = map { index( $seq, $_ ); } split( //, $encoded );
227     my $l = ( $#s + 1 ) % 4;
228     if ($l) {
229         if ( $l == 1 ) {
230             # warn "Error: Cuecat decode parsing failed!";
231             return;
232         }
233         $l = 4 - $l;
234         $#s += $l;
235     }
236     my $r = '';
237     while ( $#s >= 0 ) {
238         my $n = ( ( $s[0] << 6 | $s[1] ) << 6 | $s[2] ) << 6 | $s[3];
239         $r .=
240             chr( ( $n >> 16 ) ^ 67 )
241          .chr( ( $n >> 8 & 255 ) ^ 67 )
242          .chr( ( $n & 255 ) ^ 67 );
243         @s = @s[ 4 .. $#s ];
244     }
245     $r = substr( $r, 0, length($r) - $l );
246     return $r;
247 }
248
249 =head2 transferbook
250
251   ($dotransfer, $messages, $iteminformation) = &transferbook($newbranch, 
252                                             $barcode, $ignore_reserves);
253
254 Transfers an item to a new branch. If the item is currently on loan, it is automatically returned before the actual transfer.
255
256 C<$newbranch> is the code for the branch to which the item should be transferred.
257
258 C<$barcode> is the barcode of the item to be transferred.
259
260 If C<$ignore_reserves> is true, C<&transferbook> ignores reserves.
261 Otherwise, if an item is reserved, the transfer fails.
262
263 Returns three values:
264
265 =over
266
267 =item $dotransfer 
268
269 is true if the transfer was successful.
270
271 =item $messages
272
273 is a reference-to-hash which may have any of the following keys:
274
275 =over
276
277 =item C<BadBarcode>
278
279 There is no item in the catalog with the given barcode. The value is C<$barcode>.
280
281 =item C<DestinationEqualsHolding>
282
283 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.
284
285 =item C<WasReturned>
286
287 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.
288
289 =item C<ResFound>
290
291 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>.
292
293 =item C<WasTransferred>
294
295 The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored.
296
297 =back
298
299 =back
300
301 =cut
302
303 sub transferbook {
304     my ( $tbr, $barcode, $ignoreRs ) = @_;
305     my $messages;
306     my $dotransfer      = 1;
307     my $item = Koha::Items->find( { barcode => $barcode } );
308
309     # bad barcode..
310     unless ( $item ) {
311         $messages->{'BadBarcode'} = $barcode;
312         $dotransfer = 0;
313     }
314
315     my $itemnumber = $item->itemnumber;
316     my $issue = GetOpenIssue($itemnumber);
317     # get branches of book...
318     my $hbr = $item->homebranch;
319     my $fbr = $item->holdingbranch;
320
321     # if using Branch Transfer Limits
322     if ( C4::Context->preference("UseBranchTransferLimits") == 1 ) {
323         my $code = C4::Context->preference("BranchTransferLimitsType") eq 'ccode' ? $item->ccode : $item->biblio->biblioitem->itemtype; # BranchTransferLimitsType is 'ccode' or 'itemtype'
324         if ( C4::Context->preference("item-level_itypes") && C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ) {
325             if ( ! IsBranchTransferAllowed( $tbr, $fbr, $item->itype ) ) {
326                 $messages->{'NotAllowed'} = $tbr . "::" . $item->itype;
327                 $dotransfer = 0;
328             }
329         } elsif ( ! IsBranchTransferAllowed( $tbr, $fbr, $code ) ) {
330             $messages->{'NotAllowed'} = $tbr . "::" . $code;
331             $dotransfer = 0;
332         }
333     }
334
335     # can't transfer book if is already there....
336     if ( $fbr eq $tbr ) {
337         $messages->{'DestinationEqualsHolding'} = 1;
338         $dotransfer = 0;
339     }
340
341     # check if it is still issued to someone, return it...
342     if ( $issue ) {
343         AddReturn( $barcode, $fbr );
344         $messages->{'WasReturned'} = $issue->borrowernumber;
345     }
346
347     # find reserves.....
348     # That'll save a database query.
349     my ( $resfound, $resrec, undef ) =
350       CheckReserves( $itemnumber );
351     if ( $resfound and not $ignoreRs ) {
352         $resrec->{'ResFound'} = $resfound;
353
354         #         $messages->{'ResFound'} = $resrec;
355         $dotransfer = 1;
356     }
357
358     #actually do the transfer....
359     if ($dotransfer) {
360         ModItemTransfer( $itemnumber, $fbr, $tbr );
361
362         # don't need to update MARC anymore, we do it in batch now
363         $messages->{'WasTransfered'} = 1;
364
365     }
366     ModDateLastSeen( $itemnumber );
367     return ( $dotransfer, $messages );
368 }
369
370
371 sub TooMany {
372     my $borrower        = shift;
373     my $biblionumber = shift;
374         my $item                = shift;
375     my $params = shift;
376     my $onsite_checkout = $params->{onsite_checkout} || 0;
377     my $switch_onsite_checkout = $params->{switch_onsite_checkout} || 0;
378     my $cat_borrower    = $borrower->{'categorycode'};
379     my $dbh             = C4::Context->dbh;
380         my $branch;
381         # Get which branchcode we need
382         $branch = _GetCircControlBranch($item,$borrower);
383         my $type = (C4::Context->preference('item-level_itypes')) 
384                         ? $item->{'itype'}         # item-level
385                         : $item->{'itemtype'};     # biblio-level
386  
387     # given branch, patron category, and item type, determine
388     # applicable issuing rule
389     my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
390         {   categorycode => $cat_borrower,
391             itemtype     => $type,
392             branchcode   => $branch
393         }
394     );
395
396
397     # if a rule is found and has a loan limit set, count
398     # how many loans the patron already has that meet that
399     # rule
400     if (defined($issuing_rule) and defined($issuing_rule->maxissueqty)) {
401         my @bind_params;
402         my $count_query = q|
403             SELECT COUNT(*) AS total, COALESCE(SUM(onsite_checkout), 0) AS onsite_checkouts
404             FROM issues
405             JOIN items USING (itemnumber)
406         |;
407
408         my $rule_itemtype = $issuing_rule->itemtype;
409         if ($rule_itemtype eq "*") {
410             # matching rule has the default item type, so count only
411             # those existing loans that don't fall under a more
412             # specific rule
413             if (C4::Context->preference('item-level_itypes')) {
414                 $count_query .= " WHERE items.itype NOT IN (
415                                     SELECT itemtype FROM issuingrules
416                                     WHERE branchcode = ?
417                                     AND   (categorycode = ? OR categorycode = ?)
418                                     AND   itemtype <> '*'
419                                   ) ";
420             } else { 
421                 $count_query .= " JOIN  biblioitems USING (biblionumber) 
422                                   WHERE biblioitems.itemtype NOT IN (
423                                     SELECT itemtype FROM issuingrules
424                                     WHERE branchcode = ?
425                                     AND   (categorycode = ? OR categorycode = ?)
426                                     AND   itemtype <> '*'
427                                   ) ";
428             }
429             push @bind_params, $issuing_rule->branchcode;
430             push @bind_params, $issuing_rule->categorycode;
431             push @bind_params, $cat_borrower;
432         } else {
433             # rule has specific item type, so count loans of that
434             # specific item type
435             if (C4::Context->preference('item-level_itypes')) {
436                 $count_query .= " WHERE items.itype = ? ";
437             } else { 
438                 $count_query .= " JOIN  biblioitems USING (biblionumber) 
439                                   WHERE biblioitems.itemtype= ? ";
440             }
441             push @bind_params, $type;
442         }
443
444         $count_query .= " AND borrowernumber = ? ";
445         push @bind_params, $borrower->{'borrowernumber'};
446         my $rule_branch = $issuing_rule->branchcode;
447         if ($rule_branch ne "*") {
448             if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
449                 $count_query .= " AND issues.branchcode = ? ";
450                 push @bind_params, $branch;
451             } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
452                 ; # if branch is the patron's home branch, then count all loans by patron
453             } else {
454                 $count_query .= " AND items.homebranch = ? ";
455                 push @bind_params, $branch;
456             }
457         }
458
459         my ( $checkout_count, $onsite_checkout_count ) = $dbh->selectrow_array( $count_query, {}, @bind_params );
460
461         my $max_checkouts_allowed = $issuing_rule->maxissueqty;
462         my $max_onsite_checkouts_allowed = $issuing_rule->maxonsiteissueqty;
463
464         if ( $onsite_checkout and defined $max_onsite_checkouts_allowed ) {
465             if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed )  {
466                 return {
467                     reason => 'TOO_MANY_ONSITE_CHECKOUTS',
468                     count => $onsite_checkout_count,
469                     max_allowed => $max_onsite_checkouts_allowed,
470                 }
471             }
472         }
473         if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) {
474             my $delta = $switch_onsite_checkout ? 1 : 0;
475             if ( $checkout_count >= $max_checkouts_allowed + $delta ) {
476                 return {
477                     reason => 'TOO_MANY_CHECKOUTS',
478                     count => $checkout_count,
479                     max_allowed => $max_checkouts_allowed,
480                 };
481             }
482         } elsif ( not $onsite_checkout ) {
483             if ( $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed )  {
484                 return {
485                     reason => 'TOO_MANY_CHECKOUTS',
486                     count => $checkout_count - $onsite_checkout_count,
487                     max_allowed => $max_checkouts_allowed,
488                 };
489             }
490         }
491     }
492
493     # Now count total loans against the limit for the branch
494     my $branch_borrower_circ_rule = GetBranchBorrowerCircRule($branch, $cat_borrower);
495     if (defined($branch_borrower_circ_rule->{maxissueqty})) {
496         my @bind_params = ();
497         my $branch_count_query = q|
498             SELECT COUNT(*) AS total, COALESCE(SUM(onsite_checkout), 0) AS onsite_checkouts
499             FROM issues
500             JOIN items USING (itemnumber)
501             WHERE borrowernumber = ?
502         |;
503         push @bind_params, $borrower->{borrowernumber};
504
505         if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
506             $branch_count_query .= " AND issues.branchcode = ? ";
507             push @bind_params, $branch;
508         } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
509             ; # if branch is the patron's home branch, then count all loans by patron
510         } else {
511             $branch_count_query .= " AND items.homebranch = ? ";
512             push @bind_params, $branch;
513         }
514         my ( $checkout_count, $onsite_checkout_count ) = $dbh->selectrow_array( $branch_count_query, {}, @bind_params );
515         my $max_checkouts_allowed = $branch_borrower_circ_rule->{maxissueqty};
516         my $max_onsite_checkouts_allowed = $branch_borrower_circ_rule->{maxonsiteissueqty};
517
518         if ( $onsite_checkout and defined $max_onsite_checkouts_allowed ) {
519             if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed )  {
520                 return {
521                     reason => 'TOO_MANY_ONSITE_CHECKOUTS',
522                     count => $onsite_checkout_count,
523                     max_allowed => $max_onsite_checkouts_allowed,
524                 }
525             }
526         }
527         if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) {
528             my $delta = $switch_onsite_checkout ? 1 : 0;
529             if ( $checkout_count >= $max_checkouts_allowed + $delta ) {
530                 return {
531                     reason => 'TOO_MANY_CHECKOUTS',
532                     count => $checkout_count,
533                     max_allowed => $max_checkouts_allowed,
534                 };
535             }
536         } elsif ( not $onsite_checkout ) {
537             if ( $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed )  {
538                 return {
539                     reason => 'TOO_MANY_CHECKOUTS',
540                     count => $checkout_count - $onsite_checkout_count,
541                     max_allowed => $max_checkouts_allowed,
542                 };
543             }
544         }
545     }
546
547     if ( not defined( $issuing_rule ) and not defined($branch_borrower_circ_rule->{maxissueqty}) ) {
548         return { reason => 'NO_RULE_DEFINED', max_allowed => 0 };
549     }
550
551     # OK, the patron can issue !!!
552     return;
553 }
554
555 =head2 CanBookBeIssued
556
557   ( $issuingimpossible, $needsconfirmation, [ $alerts ] ) =  CanBookBeIssued( $patron,
558                       $barcode, $duedate, $inprocess, $ignore_reserves, $params );
559
560 Check if a book can be issued.
561
562 C<$issuingimpossible> and C<$needsconfirmation> are hashrefs.
563
564 IMPORTANT: The assumption by users of this routine is that causes blocking
565 the issue are keyed by uppercase labels and other returned
566 data is keyed in lower case!
567
568 =over 4
569
570 =item C<$patron> is a Koha::Patron
571
572 =item C<$barcode> is the bar code of the book being issued.
573
574 =item C<$duedates> is a DateTime object.
575
576 =item C<$inprocess> boolean switch
577
578 =item C<$ignore_reserves> boolean switch
579
580 =item C<$params> Hashref of additional parameters
581
582 Available keys:
583     override_high_holds - Ignore high holds
584     onsite_checkout     - Checkout is an onsite checkout that will not leave the library
585
586 =back
587
588 Returns :
589
590 =over 4
591
592 =item C<$issuingimpossible> a reference to a hash. It contains reasons why issuing is impossible.
593 Possible values are :
594
595 =back
596
597 =head3 INVALID_DATE 
598
599 sticky due date is invalid
600
601 =head3 GNA
602
603 borrower gone with no address
604
605 =head3 CARD_LOST
606
607 borrower declared it's card lost
608
609 =head3 DEBARRED
610
611 borrower debarred
612
613 =head3 UNKNOWN_BARCODE
614
615 barcode unknown
616
617 =head3 NOT_FOR_LOAN
618
619 item is not for loan
620
621 =head3 WTHDRAWN
622
623 item withdrawn.
624
625 =head3 RESTRICTED
626
627 item is restricted (set by ??)
628
629 C<$needsconfirmation> a reference to a hash. It contains reasons why the loan 
630 could be prevented, but ones that can be overriden by the operator.
631
632 Possible values are :
633
634 =head3 DEBT
635
636 borrower has debts.
637
638 =head3 RENEW_ISSUE
639
640 renewing, not issuing
641
642 =head3 ISSUED_TO_ANOTHER
643
644 issued to someone else.
645
646 =head3 RESERVED
647
648 reserved for someone else.
649
650 =head3 INVALID_DATE
651
652 sticky due date is invalid or due date in the past
653
654 =head3 TOO_MANY
655
656 if the borrower borrows to much things
657
658 =cut
659
660 sub CanBookBeIssued {
661     my ( $patron, $barcode, $duedate, $inprocess, $ignore_reserves, $params ) = @_;
662     my %needsconfirmation;    # filled with problems that needs confirmations
663     my %issuingimpossible;    # filled with problems that causes the issue to be IMPOSSIBLE
664     my %alerts;               # filled with messages that shouldn't stop issuing, but the librarian should be aware of.
665     my %messages;             # filled with information messages that should be displayed.
666
667     my $onsite_checkout     = $params->{onsite_checkout}     || 0;
668     my $override_high_holds = $params->{override_high_holds} || 0;
669
670     my $item = GetItem(undef, $barcode );
671     my $issue = Koha::Checkouts->find( { itemnumber => $item->{itemnumber} } );
672         my $biblioitem = GetBiblioItemData($item->{biblioitemnumber});
673         $item->{'itemtype'}=$item->{'itype'}; 
674     my $dbh             = C4::Context->dbh;
675     my $patron_unblessed = $patron->unblessed;
676
677     # MANDATORY CHECKS - unless item exists, nothing else matters
678     unless ( $item->{barcode} ) {
679         $issuingimpossible{UNKNOWN_BARCODE} = 1;
680     }
681         return ( \%issuingimpossible, \%needsconfirmation ) if %issuingimpossible;
682
683     #
684     # DUE DATE is OK ? -- should already have checked.
685     #
686     if ($duedate && ref $duedate ne 'DateTime') {
687         $duedate = dt_from_string($duedate);
688     }
689     my $now = DateTime->now( time_zone => C4::Context->tz() );
690     unless ( $duedate ) {
691         my $issuedate = $now->clone();
692
693         my $branch = _GetCircControlBranch($item, $patron_unblessed);
694         my $itype = ( C4::Context->preference('item-level_itypes') ) ? $item->{'itype'} : $biblioitem->{'itemtype'};
695         $duedate = CalcDateDue( $issuedate, $itype, $branch, $patron_unblessed );
696
697         # Offline circ calls AddIssue directly, doesn't run through here
698         #  So issuingimpossible should be ok.
699     }
700     if ($duedate) {
701         my $today = $now->clone();
702         $today->truncate( to => 'minute');
703         if (DateTime->compare($duedate,$today) == -1 ) { # duedate cannot be before now
704             $needsconfirmation{INVALID_DATE} = output_pref($duedate);
705         }
706     } else {
707             $issuingimpossible{INVALID_DATE} = output_pref($duedate);
708     }
709
710     #
711     # BORROWER STATUS
712     #
713     if ( $patron->category->category_type eq 'X' && (  $item->{barcode}  )) {
714         # stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1  .
715         &UpdateStats({
716                      branch => C4::Context->userenv->{'branch'},
717                      type => 'localuse',
718                      itemnumber => $item->{'itemnumber'},
719                      itemtype => $item->{'itype'},
720                      borrowernumber => $patron->borrowernumber,
721                      ccode => $item->{'ccode'}}
722                     );
723         ModDateLastSeen( $item->{'itemnumber'} );
724         return( { STATS => 1 }, {});
725     }
726
727     my $flags = C4::Members::patronflags( $patron_unblessed );
728     if ( ref $flags ) {
729         if ( $flags->{GNA} ) {
730             $issuingimpossible{GNA} = 1;
731         }
732         if ( $flags->{'LOST'} ) {
733             $issuingimpossible{CARD_LOST} = 1;
734         }
735         if ( $flags->{'DBARRED'} ) {
736             $issuingimpossible{DEBARRED} = 1;
737         }
738     }
739
740     if ( $patron->is_expired ) {
741         $issuingimpossible{EXPIRED} = 1;
742     }
743
744     #
745     # BORROWER STATUS
746     #
747
748     # DEBTS
749     my $account = $patron->account;
750     my $balance = $account->balance;
751     my $non_issues_charges = $account->non_issues_charges;
752     my $other_charges = $balance - $non_issues_charges;
753
754     my $amountlimit = C4::Context->preference("noissuescharge");
755     my $allowfineoverride = C4::Context->preference("AllowFineOverride");
756     my $allfinesneedoverride = C4::Context->preference("AllFinesNeedOverride");
757
758     # Check the debt of this patrons guarantees
759     my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees");
760     $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees );
761     if ( defined $no_issues_charge_guarantees ) {
762         my @guarantees = $patron->guarantees();
763         my $guarantees_non_issues_charges;
764         foreach my $g ( @guarantees ) {
765             $guarantees_non_issues_charges += $g->account->non_issues_charges;
766         }
767
768         if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && !$allowfineoverride) {
769             $issuingimpossible{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
770         } elsif ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && $allowfineoverride) {
771             $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
772         } elsif ( $allfinesneedoverride && $guarantees_non_issues_charges > 0 && $guarantees_non_issues_charges <= $no_issues_charge_guarantees && !$inprocess ) {
773             $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges;
774         }
775     }
776
777     if ( C4::Context->preference("IssuingInProcess") ) {
778         if ( $non_issues_charges > $amountlimit && !$inprocess && !$allowfineoverride) {
779             $issuingimpossible{DEBT} = $non_issues_charges;
780         } elsif ( $non_issues_charges > $amountlimit && !$inprocess && $allowfineoverride) {
781             $needsconfirmation{DEBT} = $non_issues_charges;
782         } elsif ( $allfinesneedoverride && $non_issues_charges > 0 && $non_issues_charges <= $amountlimit && !$inprocess ) {
783             $needsconfirmation{DEBT} = $non_issues_charges;
784         }
785     }
786     else {
787         if ( $non_issues_charges > $amountlimit && $allowfineoverride ) {
788             $needsconfirmation{DEBT} = $non_issues_charges;
789         } elsif ( $non_issues_charges > $amountlimit && !$allowfineoverride) {
790             $issuingimpossible{DEBT} = $non_issues_charges;
791         } elsif ( $non_issues_charges > 0 && $allfinesneedoverride ) {
792             $needsconfirmation{DEBT} = $non_issues_charges;
793         }
794     }
795
796     if ($balance > 0 && $other_charges > 0) {
797         $alerts{OTHER_CHARGES} = sprintf( "%.2f", $other_charges );
798     }
799
800     $patron = Koha::Patrons->find( $patron->borrowernumber ); # FIXME Refetch just in case, to avoid regressions. But must not be needed
801     $patron_unblessed = $patron->unblessed;
802
803     if ( my $debarred_date = $patron->is_debarred ) {
804          # patron has accrued fine days or has a restriction. $count is a date
805         if ($debarred_date eq '9999-12-31') {
806             $issuingimpossible{USERBLOCKEDNOENDDATE} = $debarred_date;
807         }
808         else {
809             $issuingimpossible{USERBLOCKEDWITHENDDATE} = $debarred_date;
810         }
811     } elsif ( my $num_overdues = $patron->has_overdues ) {
812         ## patron has outstanding overdue loans
813         if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){
814             $issuingimpossible{USERBLOCKEDOVERDUE} = $num_overdues;
815         }
816         elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){
817             $needsconfirmation{USERBLOCKEDOVERDUE} = $num_overdues;
818         }
819     }
820
821     #
822     # CHECK IF BOOK ALREADY ISSUED TO THIS BORROWER
823     #
824     if ( $issue && $issue->borrowernumber eq $patron->borrowernumber ){
825
826         # Already issued to current borrower.
827         # If it is an on-site checkout if it can be switched to a normal checkout
828         # or ask whether the loan should be renewed
829
830         if ( $issue->onsite_checkout
831                 and C4::Context->preference('SwitchOnSiteCheckouts') ) {
832             $messages{ONSITE_CHECKOUT_WILL_BE_SWITCHED} = 1;
833         } else {
834             my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed(
835                 $patron->borrowernumber,
836                 $item->{'itemnumber'},
837             );
838             if ( $CanBookBeRenewed == 0 ) {    # no more renewals allowed
839                 if ( $renewerror eq 'onsite_checkout' ) {
840                     $issuingimpossible{NO_RENEWAL_FOR_ONSITE_CHECKOUTS} = 1;
841                 }
842                 else {
843                     $issuingimpossible{NO_MORE_RENEWALS} = 1;
844                 }
845             }
846             else {
847                 $needsconfirmation{RENEW_ISSUE} = 1;
848             }
849         }
850     }
851     elsif ( $issue ) {
852
853         # issued to someone else
854
855         my $patron = Koha::Patrons->find( $issue->borrowernumber );
856
857         my ( $can_be_returned, $message ) = CanBookBeReturned( $item, C4::Context->userenv->{branch} );
858
859         unless ( $can_be_returned ) {
860             $issuingimpossible{RETURN_IMPOSSIBLE} = 1;
861             $issuingimpossible{branch_to_return} = $message;
862         } else {
863             $needsconfirmation{ISSUED_TO_ANOTHER} = 1;
864             $needsconfirmation{issued_firstname} = $patron->firstname;
865             $needsconfirmation{issued_surname} = $patron->surname;
866             $needsconfirmation{issued_cardnumber} = $patron->cardnumber;
867             $needsconfirmation{issued_borrowernumber} = $patron->borrowernumber;
868         }
869     }
870
871     # JB34 CHECKS IF BORROWERS DON'T HAVE ISSUE TOO MANY BOOKS
872     #
873     my $switch_onsite_checkout = (
874           C4::Context->preference('SwitchOnSiteCheckouts')
875       and $issue
876       and $issue->onsite_checkout
877       and $issue->borrowernumber == $patron->borrowernumber ? 1 : 0 );
878     my $toomany = TooMany( $patron_unblessed, $item->{biblionumber}, $item, { onsite_checkout => $onsite_checkout, switch_onsite_checkout => $switch_onsite_checkout, } );
879     # if TooMany max_allowed returns 0 the user doesn't have permission to check out this book
880     if ( $toomany && not exists $needsconfirmation{RENEW_ISSUE} ) {
881         if ( $toomany->{max_allowed} == 0 ) {
882             $needsconfirmation{PATRON_CANT} = 1;
883         }
884         if ( C4::Context->preference("AllowTooManyOverride") ) {
885             $needsconfirmation{TOO_MANY} = $toomany->{reason};
886             $needsconfirmation{current_loan_count} = $toomany->{count};
887             $needsconfirmation{max_loans_allowed} = $toomany->{max_allowed};
888         } else {
889             $issuingimpossible{TOO_MANY} = $toomany->{reason};
890             $issuingimpossible{current_loan_count} = $toomany->{count};
891             $issuingimpossible{max_loans_allowed} = $toomany->{max_allowed};
892         }
893     }
894
895     #
896     # CHECKPREVCHECKOUT: CHECK IF ITEM HAS EVER BEEN LENT TO PATRON
897     #
898     $patron = Koha::Patrons->find( $patron->borrowernumber ); # FIXME Refetch just in case, to avoid regressions. But must not be needed
899     my $wants_check = $patron->wants_check_for_previous_checkout;
900     $needsconfirmation{PREVISSUE} = 1
901         if ($wants_check and $patron->do_check_for_previous_checkout($item));
902
903     #
904     # ITEM CHECKING
905     #
906     if ( $item->{'notforloan'} )
907     {
908         if(!C4::Context->preference("AllowNotForLoanOverride")){
909             $issuingimpossible{NOT_FOR_LOAN} = 1;
910             $issuingimpossible{item_notforloan} = $item->{'notforloan'};
911         }else{
912             $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
913             $needsconfirmation{item_notforloan} = $item->{'notforloan'};
914         }
915     }
916     else {
917         # we have to check itemtypes.notforloan also
918         if (C4::Context->preference('item-level_itypes')){
919             # this should probably be a subroutine
920             my $sth = $dbh->prepare("SELECT notforloan FROM itemtypes WHERE itemtype = ?");
921             $sth->execute($item->{'itemtype'});
922             my $notforloan=$sth->fetchrow_hashref();
923             if ($notforloan->{'notforloan'}) {
924                 if (!C4::Context->preference("AllowNotForLoanOverride")) {
925                     $issuingimpossible{NOT_FOR_LOAN} = 1;
926                     $issuingimpossible{itemtype_notforloan} = $item->{'itype'};
927                 } else {
928                     $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
929                     $needsconfirmation{itemtype_notforloan} = $item->{'itype'};
930                 }
931             }
932         }
933         elsif ($biblioitem->{'notforloan'} == 1){
934             if (!C4::Context->preference("AllowNotForLoanOverride")) {
935                 $issuingimpossible{NOT_FOR_LOAN} = 1;
936                 $issuingimpossible{itemtype_notforloan} = $biblioitem->{'itemtype'};
937             } else {
938                 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
939                 $needsconfirmation{itemtype_notforloan} = $biblioitem->{'itemtype'};
940             }
941         }
942     }
943     if ( $item->{'withdrawn'} && $item->{'withdrawn'} > 0 )
944     {
945         $issuingimpossible{WTHDRAWN} = 1;
946     }
947     if (   $item->{'restricted'}
948         && $item->{'restricted'} == 1 )
949     {
950         $issuingimpossible{RESTRICTED} = 1;
951     }
952     if ( $item->{'itemlost'} && C4::Context->preference("IssueLostItem") ne 'nothing' ) {
953         my $av = Koha::AuthorisedValues->search({ category => 'LOST', authorised_value => $item->{itemlost} });
954         my $code = $av->count ? $av->next->lib : '';
955         $needsconfirmation{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'confirm' );
956         $alerts{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'alert' );
957     }
958     if ( C4::Context->preference("IndependentBranches") ) {
959         my $userenv = C4::Context->userenv;
960         unless ( C4::Context->IsSuperLibrarian() ) {
961             if ( $item->{C4::Context->preference("HomeOrHoldingBranch")} ne $userenv->{branch} ){
962                 $issuingimpossible{ITEMNOTSAMEBRANCH} = 1;
963                 $issuingimpossible{'itemhomebranch'} = $item->{C4::Context->preference("HomeOrHoldingBranch")};
964             }
965             $needsconfirmation{BORRNOTSAMEBRANCH} = $patron->branchcode
966               if ( $patron->branchcode ne $userenv->{branch} );
967         }
968     }
969     #
970     # CHECK IF THERE IS RENTAL CHARGES. RENTAL MUST BE CONFIRMED BY THE BORROWER
971     #
972     my $rentalConfirmation = C4::Context->preference("RentalFeesCheckoutConfirmation");
973
974     if ( $rentalConfirmation ){
975         my ($rentalCharge) = GetIssuingCharges( $item->{'itemnumber'}, $patron->borrowernumber );
976         if ( $rentalCharge > 0 ){
977             $needsconfirmation{RENTALCHARGE} = $rentalCharge;
978         }
979     }
980
981     unless ( $ignore_reserves ) {
982         # See if the item is on reserve.
983         my ( $restype, $res ) = C4::Reserves::CheckReserves( $item->{'itemnumber'} );
984         if ($restype) {
985             my $resbor = $res->{'borrowernumber'};
986             if ( $resbor ne $patron->borrowernumber ) {
987                 my $patron = Koha::Patrons->find( $resbor );
988                 if ( $restype eq "Waiting" )
989                 {
990                     # The item is on reserve and waiting, but has been
991                     # reserved by some other patron.
992                     $needsconfirmation{RESERVE_WAITING} = 1;
993                     $needsconfirmation{'resfirstname'} = $patron->firstname;
994                     $needsconfirmation{'ressurname'} = $patron->surname;
995                     $needsconfirmation{'rescardnumber'} = $patron->cardnumber;
996                     $needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
997                     $needsconfirmation{'resbranchcode'} = $res->{branchcode};
998                     $needsconfirmation{'reswaitingdate'} = $res->{'waitingdate'};
999                 }
1000                 elsif ( $restype eq "Reserved" ) {
1001                     # The item is on reserve for someone else.
1002                     $needsconfirmation{RESERVED} = 1;
1003                     $needsconfirmation{'resfirstname'} = $patron->firstname;
1004                     $needsconfirmation{'ressurname'} = $patron->surname;
1005                     $needsconfirmation{'rescardnumber'} = $patron->cardnumber;
1006                     $needsconfirmation{'resborrowernumber'} = $patron->borrowernumber;
1007                     $needsconfirmation{'resbranchcode'} = $patron->branchcode;
1008                     $needsconfirmation{'resreservedate'} = $res->{reservedate};
1009                 }
1010             }
1011         }
1012     }
1013
1014     ## CHECK AGE RESTRICTION
1015     my $agerestriction  = $biblioitem->{'agerestriction'};
1016     my ($restriction_age, $daysToAgeRestriction) = GetAgeRestriction( $agerestriction, $patron->unblessed );
1017     if ( $daysToAgeRestriction && $daysToAgeRestriction > 0 ) {
1018         if ( C4::Context->preference('AgeRestrictionOverride') ) {
1019             $needsconfirmation{AGE_RESTRICTION} = "$agerestriction";
1020         }
1021         else {
1022             $issuingimpossible{AGE_RESTRICTION} = "$agerestriction";
1023         }
1024     }
1025
1026     ## check for high holds decreasing loan period
1027     if ( C4::Context->preference('decreaseLoanHighHolds') ) {
1028         my $check = checkHighHolds( $item, $patron_unblessed );
1029
1030         if ( $check->{exceeded} ) {
1031             if ($override_high_holds) {
1032                 $alerts{HIGHHOLDS} = {
1033                     num_holds  => $check->{outstanding},
1034                     duration   => $check->{duration},
1035                     returndate => output_pref( $check->{due_date} ),
1036                 };
1037             }
1038             else {
1039                 $needsconfirmation{HIGHHOLDS} = {
1040                     num_holds  => $check->{outstanding},
1041                     duration   => $check->{duration},
1042                     returndate => output_pref( $check->{due_date} ),
1043                 };
1044             }
1045         }
1046     }
1047
1048     if (
1049         !C4::Context->preference('AllowMultipleIssuesOnABiblio') &&
1050         # don't do the multiple loans per bib check if we've
1051         # already determined that we've got a loan on the same item
1052         !$issuingimpossible{NO_MORE_RENEWALS} &&
1053         !$needsconfirmation{RENEW_ISSUE}
1054     ) {
1055         # Check if borrower has already issued an item from the same biblio
1056         # Only if it's not a subscription
1057         my $biblionumber = $item->{biblionumber};
1058         require C4::Serials;
1059         my $is_a_subscription = C4::Serials::CountSubscriptionFromBiblionumber($biblionumber);
1060         unless ($is_a_subscription) {
1061             # FIXME Should be $patron->checkouts($args);
1062             my $checkouts = Koha::Checkouts->search(
1063                 {
1064                     borrowernumber => $patron->borrowernumber,
1065                     biblionumber   => $biblionumber,
1066                 },
1067                 {
1068                     join => 'item',
1069                 }
1070             );
1071             # if we get here, we don't already have a loan on this item,
1072             # so if there are any loans on this bib, ask for confirmation
1073             if ( $checkouts->count ) {
1074                 $needsconfirmation{BIBLIO_ALREADY_ISSUED} = 1;
1075             }
1076         }
1077     }
1078
1079     return ( \%issuingimpossible, \%needsconfirmation, \%alerts, \%messages, );
1080 }
1081
1082 =head2 CanBookBeReturned
1083
1084   ($returnallowed, $message) = CanBookBeReturned($item, $branch)
1085
1086 Check whether the item can be returned to the provided branch
1087
1088 =over 4
1089
1090 =item C<$item> is a hash of item information as returned from GetItem
1091
1092 =item C<$branch> is the branchcode where the return is taking place
1093
1094 =back
1095
1096 Returns:
1097
1098 =over 4
1099
1100 =item C<$returnallowed> is 0 or 1, corresponding to whether the return is allowed (1) or not (0)
1101
1102 =item C<$message> is the branchcode where the item SHOULD be returned, if the return is not allowed
1103
1104 =back
1105
1106 =cut
1107
1108 sub CanBookBeReturned {
1109   my ($item, $branch) = @_;
1110   my $allowreturntobranch = C4::Context->preference("AllowReturnToBranch") || 'anywhere';
1111
1112   # assume return is allowed to start
1113   my $allowed = 1;
1114   my $message;
1115
1116   # identify all cases where return is forbidden
1117   if ($allowreturntobranch eq 'homebranch' && $branch ne $item->{'homebranch'}) {
1118      $allowed = 0;
1119      $message = $item->{'homebranch'};
1120   } elsif ($allowreturntobranch eq 'holdingbranch' && $branch ne $item->{'holdingbranch'}) {
1121      $allowed = 0;
1122      $message = $item->{'holdingbranch'};
1123   } elsif ($allowreturntobranch eq 'homeorholdingbranch' && $branch ne $item->{'homebranch'} && $branch ne $item->{'holdingbranch'}) {
1124      $allowed = 0;
1125      $message = $item->{'homebranch'}; # FIXME: choice of homebranch is arbitrary
1126   }
1127
1128   return ($allowed, $message);
1129 }
1130
1131 =head2 CheckHighHolds
1132
1133     used when syspref decreaseLoanHighHolds is active. Returns 1 or 0 to define whether the minimum value held in
1134     decreaseLoanHighHoldsValue is exceeded, the total number of outstanding holds, the number of days the loan
1135     has been decreased to (held in syspref decreaseLoanHighHoldsValue), and the new due date
1136
1137 =cut
1138
1139 sub checkHighHolds {
1140     my ( $item, $borrower ) = @_;
1141     my $branch = _GetCircControlBranch( $item, $borrower );
1142     my $item_object = Koha::Items->find( $item->{itemnumber} );
1143
1144     my $return_data = {
1145         exceeded    => 0,
1146         outstanding => 0,
1147         duration    => 0,
1148         due_date    => undef,
1149     };
1150
1151     my $holds = Koha::Holds->search( { biblionumber => $item->{'biblionumber'} } );
1152
1153     if ( $holds->count() ) {
1154         $return_data->{outstanding} = $holds->count();
1155
1156         my $decreaseLoanHighHoldsControl        = C4::Context->preference('decreaseLoanHighHoldsControl');
1157         my $decreaseLoanHighHoldsValue          = C4::Context->preference('decreaseLoanHighHoldsValue');
1158         my $decreaseLoanHighHoldsIgnoreStatuses = C4::Context->preference('decreaseLoanHighHoldsIgnoreStatuses');
1159
1160         my @decreaseLoanHighHoldsIgnoreStatuses = split( /,/, $decreaseLoanHighHoldsIgnoreStatuses );
1161
1162         if ( $decreaseLoanHighHoldsControl eq 'static' ) {
1163
1164             # static means just more than a given number of holds on the record
1165
1166             # If the number of holds is less than the threshold, we can stop here
1167             if ( $holds->count() < $decreaseLoanHighHoldsValue ) {
1168                 return $return_data;
1169             }
1170         }
1171         elsif ( $decreaseLoanHighHoldsControl eq 'dynamic' ) {
1172
1173             # dynamic means X more than the number of holdable items on the record
1174
1175             # let's get the items
1176             my @items = $holds->next()->biblio()->items();
1177
1178             # Remove any items with status defined to be ignored even if the would not make item unholdable
1179             foreach my $status (@decreaseLoanHighHoldsIgnoreStatuses) {
1180                 @items = grep { !$_->$status } @items;
1181             }
1182
1183             # Remove any items that are not holdable for this patron
1184             @items = grep { CanItemBeReserved( $borrower->{borrowernumber}, $_->itemnumber ) eq 'OK' } @items;
1185
1186             my $items_count = scalar @items;
1187
1188             my $threshold = $items_count + $decreaseLoanHighHoldsValue;
1189
1190             # If the number of holds is less than the count of items we have
1191             # plus the number of holds allowed above that count, we can stop here
1192             if ( $holds->count() <= $threshold ) {
1193                 return $return_data;
1194             }
1195         }
1196
1197         my $issuedate = DateTime->now( time_zone => C4::Context->tz() );
1198
1199         my $calendar = Koha::Calendar->new( branchcode => $branch );
1200
1201         my $itype = $item_object->effective_itemtype;
1202         my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branch, $borrower );
1203
1204         my $decreaseLoanHighHoldsDuration = C4::Context->preference('decreaseLoanHighHoldsDuration');
1205
1206         my $reduced_datedue = $calendar->addDate( $issuedate, $decreaseLoanHighHoldsDuration );
1207         $reduced_datedue->set_hour($orig_due->hour);
1208         $reduced_datedue->set_minute($orig_due->minute);
1209         $reduced_datedue->truncate( to => 'minute' );
1210
1211         if ( DateTime->compare( $reduced_datedue, $orig_due ) == -1 ) {
1212             $return_data->{exceeded} = 1;
1213             $return_data->{duration} = $decreaseLoanHighHoldsDuration;
1214             $return_data->{due_date} = $reduced_datedue;
1215         }
1216     }
1217
1218     return $return_data;
1219 }
1220
1221 =head2 AddIssue
1222
1223   &AddIssue($borrower, $barcode, [$datedue], [$cancelreserve], [$issuedate])
1224
1225 Issue a book. Does no check, they are done in CanBookBeIssued. If we reach this sub, it means the user confirmed if needed.
1226
1227 =over 4
1228
1229 =item C<$borrower> is a hash with borrower informations (from Koha::Patron->unblessed).
1230
1231 =item C<$barcode> is the barcode of the item being issued.
1232
1233 =item C<$datedue> is a DateTime object for the max date of return, i.e. the date due (optional).
1234 Calculated if empty.
1235
1236 =item C<$cancelreserve> is 1 to override and cancel any pending reserves for the item (optional).
1237
1238 =item C<$issuedate> is the date to issue the item in iso (YYYY-MM-DD) format (optional).
1239 Defaults to today.  Unlike C<$datedue>, NOT a DateTime object, unfortunately.
1240
1241 AddIssue does the following things :
1242
1243   - step 01: check that there is a borrowernumber & a barcode provided
1244   - check for RENEWAL (book issued & being issued to the same patron)
1245       - renewal YES = Calculate Charge & renew
1246       - renewal NO  =
1247           * BOOK ACTUALLY ISSUED ? do a return if book is actually issued (but to someone else)
1248           * RESERVE PLACED ?
1249               - fill reserve if reserve to this patron
1250               - cancel reserve or not, otherwise
1251           * TRANSFERT PENDING ?
1252               - complete the transfert
1253           * ISSUE THE BOOK
1254
1255 =back
1256
1257 =cut
1258
1259 sub AddIssue {
1260     my ( $borrower, $barcode, $datedue, $cancelreserve, $issuedate, $sipmode, $params ) = @_;
1261
1262     my $onsite_checkout = $params && $params->{onsite_checkout} ? 1 : 0;
1263     my $switch_onsite_checkout = $params && $params->{switch_onsite_checkout};
1264     my $auto_renew = $params && $params->{auto_renew};
1265     my $dbh          = C4::Context->dbh;
1266     my $barcodecheck = CheckValidBarcode($barcode);
1267
1268     my $issue;
1269
1270     if ( $datedue && ref $datedue ne 'DateTime' ) {
1271         $datedue = dt_from_string($datedue);
1272     }
1273
1274     # $issuedate defaults to today.
1275     if ( !defined $issuedate ) {
1276         $issuedate = DateTime->now( time_zone => C4::Context->tz() );
1277     }
1278     else {
1279         if ( ref $issuedate ne 'DateTime' ) {
1280             $issuedate = dt_from_string($issuedate);
1281
1282         }
1283     }
1284
1285     # Stop here if the patron or barcode doesn't exist
1286     if ( $borrower && $barcode && $barcodecheck ) {
1287         # find which item we issue
1288         my $item = GetItem( '', $barcode )
1289           or return;    # if we don't get an Item, abort.
1290         my $item_object = Koha::Items->find( { barcode => $barcode } );
1291
1292         my $branch = _GetCircControlBranch( $item, $borrower );
1293
1294         # get actual issuing if there is one
1295         my $actualissue = Koha::Checkouts->find( { itemnumber => $item->{itemnumber} } );
1296
1297         # check if we just renew the issue.
1298         if ( $actualissue and $actualissue->borrowernumber eq $borrower->{'borrowernumber'}
1299                 and not $switch_onsite_checkout ) {
1300             $datedue = AddRenewal(
1301                 $borrower->{'borrowernumber'},
1302                 $item->{'itemnumber'},
1303                 $branch,
1304                 $datedue,
1305                 $issuedate,    # here interpreted as the renewal date
1306             );
1307         }
1308         else {
1309             # it's NOT a renewal
1310             if ( $actualissue and not $switch_onsite_checkout ) {
1311                 # This book is currently on loan, but not to the person
1312                 # who wants to borrow it now. mark it returned before issuing to the new borrower
1313                 my ( $allowed, $message ) = CanBookBeReturned( $item, C4::Context->userenv->{branch} );
1314                 return unless $allowed;
1315                 AddReturn( $item->{'barcode'}, C4::Context->userenv->{'branch'} );
1316             }
1317
1318             MoveReserve( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $cancelreserve );
1319
1320             # Starting process for transfer job (checking transfert and validate it if we have one)
1321             my ($datesent) = GetTransfers( $item->{'itemnumber'} );
1322             if ($datesent) {
1323                 # updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....)
1324                 my $sth = $dbh->prepare(
1325                     "UPDATE branchtransfers 
1326                         SET datearrived = now(),
1327                         tobranch = ?,
1328                         comments = 'Forced branchtransfer'
1329                     WHERE itemnumber= ? AND datearrived IS NULL"
1330                 );
1331                 $sth->execute( C4::Context->userenv->{'branch'},
1332                     $item->{'itemnumber'} );
1333             }
1334
1335             # If automatic renewal wasn't selected while issuing, set the value according to the issuing rule.
1336             unless ($auto_renew) {
1337                 my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
1338                     {   categorycode => $borrower->{categorycode},
1339                         itemtype     => $item->{itype},
1340                         branchcode   => $branch
1341                     }
1342                 );
1343
1344                 $auto_renew = $issuing_rule->auto_renew if $issuing_rule;
1345             }
1346
1347             # Record in the database the fact that the book was issued.
1348             unless ($datedue) {
1349                 my $itype = $item_object->effective_itemtype;
1350                 $datedue = CalcDateDue( $issuedate, $itype, $branch, $borrower );
1351
1352             }
1353             $datedue->truncate( to => 'minute' );
1354
1355             $issue = Koha::Database->new()->schema()->resultset('Issue')->update_or_create(
1356                 {
1357                     borrowernumber => $borrower->{'borrowernumber'},
1358                     itemnumber     => $item->{'itemnumber'},
1359                     issuedate      => $issuedate->strftime('%Y-%m-%d %H:%M:%S'),
1360                     date_due       => $datedue->strftime('%Y-%m-%d %H:%M:%S'),
1361                     branchcode     => C4::Context->userenv->{'branch'},
1362                     onsite_checkout => $onsite_checkout,
1363                     auto_renew      => $auto_renew ? 1 : 0
1364                 }
1365               );
1366
1367             if ( C4::Context->preference('ReturnToShelvingCart') ) {
1368                 # ReturnToShelvingCart is on, anything issued should be taken off the cart.
1369                 CartToShelf( $item->{'itemnumber'} );
1370             }
1371             $item->{'issues'}++;
1372             if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) {
1373                 UpdateTotalIssues( $item->{'biblionumber'}, 1 );
1374             }
1375
1376             ## If item was lost, it has now been found, reverse any list item charges if necessary.
1377             if ( $item->{'itemlost'} ) {
1378                 if (
1379                     Koha::RefundLostItemFeeRules->should_refund(
1380                         {
1381                             current_branch      => C4::Context->userenv->{branch},
1382                             item_home_branch    => $item->{homebranch},
1383                             item_holding_branch => $item->{holdingbranch}
1384                         }
1385                     )
1386                   )
1387                 {
1388                     _FixAccountForLostAndReturned( $item->{'itemnumber'}, undef,
1389                         $item->{'barcode'} );
1390                 }
1391             }
1392
1393             ModItem(
1394                 {
1395                     issues        => $item->{'issues'},
1396                     holdingbranch => C4::Context->userenv->{'branch'},
1397                     itemlost      => 0,
1398                     onloan        => $datedue->ymd(),
1399                     datelastborrowed => DateTime->now( time_zone => C4::Context->tz() )->ymd(),
1400                 },
1401                 $item->{'biblionumber'},
1402                 $item->{'itemnumber'}
1403             );
1404             ModDateLastSeen( $item->{'itemnumber'} );
1405
1406            # If it costs to borrow this book, charge it to the patron's account.
1407             my ( $charge, $itemtype ) = GetIssuingCharges( $item->{'itemnumber'}, $borrower->{'borrowernumber'} );
1408             if ( $charge > 0 ) {
1409                 AddIssuingCharge( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $charge );
1410                 $item->{'charge'} = $charge;
1411             }
1412
1413             # Record the fact that this book was issued.
1414             &UpdateStats(
1415                 {
1416                     branch => C4::Context->userenv->{'branch'},
1417                     type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ),
1418                     amount         => $charge,
1419                     other          => ( $sipmode ? "SIP-$sipmode" : '' ),
1420                     itemnumber     => $item->{'itemnumber'},
1421                     itemtype       => $item->{'itype'},
1422                     location       => $item->{location},
1423                     borrowernumber => $borrower->{'borrowernumber'},
1424                     ccode          => $item->{'ccode'}
1425                 }
1426             );
1427
1428             # Send a checkout slip.
1429             my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1430             my %conditions        = (
1431                 branchcode   => $branch,
1432                 categorycode => $borrower->{categorycode},
1433                 item_type    => $item->{itype},
1434                 notification => 'CHECKOUT',
1435             );
1436             if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
1437                 SendCirculationAlert(
1438                     {
1439                         type     => 'CHECKOUT',
1440                         item     => $item,
1441                         borrower => $borrower,
1442                         branch   => $branch,
1443                     }
1444                 );
1445             }
1446             logaction(
1447                 "CIRCULATION", "ISSUE",
1448                 $borrower->{'borrowernumber'},
1449                 $item->{'itemnumber'}
1450             ) if C4::Context->preference("IssueLog");
1451         }
1452     }
1453     return $issue;
1454 }
1455
1456 =head2 GetLoanLength
1457
1458   my $loanlength = &GetLoanLength($borrowertype,$itemtype,branchcode)
1459
1460 Get loan length for an itemtype, a borrower type and a branch
1461
1462 =cut
1463
1464 sub GetLoanLength {
1465     my ( $borrowertype, $itemtype, $branchcode ) = @_;
1466     my $dbh = C4::Context->dbh;
1467     my $sth = $dbh->prepare(qq{
1468         SELECT issuelength, lengthunit, renewalperiod
1469         FROM issuingrules
1470         WHERE   categorycode=?
1471             AND itemtype=?
1472             AND branchcode=?
1473             AND issuelength IS NOT NULL
1474     });
1475
1476     # try to find issuelength & return the 1st available.
1477     # check with borrowertype, itemtype and branchcode, then without one of those parameters
1478     $sth->execute( $borrowertype, $itemtype, $branchcode );
1479     my $loanlength = $sth->fetchrow_hashref;
1480
1481     return $loanlength
1482       if defined($loanlength) && defined $loanlength->{issuelength};
1483
1484     $sth->execute( $borrowertype, '*', $branchcode );
1485     $loanlength = $sth->fetchrow_hashref;
1486     return $loanlength
1487       if defined($loanlength) && defined $loanlength->{issuelength};
1488
1489     $sth->execute( '*', $itemtype, $branchcode );
1490     $loanlength = $sth->fetchrow_hashref;
1491     return $loanlength
1492       if defined($loanlength) && defined $loanlength->{issuelength};
1493
1494     $sth->execute( '*', '*', $branchcode );
1495     $loanlength = $sth->fetchrow_hashref;
1496     return $loanlength
1497       if defined($loanlength) && defined $loanlength->{issuelength};
1498
1499     $sth->execute( $borrowertype, $itemtype, '*' );
1500     $loanlength = $sth->fetchrow_hashref;
1501     return $loanlength
1502       if defined($loanlength) && defined $loanlength->{issuelength};
1503
1504     $sth->execute( $borrowertype, '*', '*' );
1505     $loanlength = $sth->fetchrow_hashref;
1506     return $loanlength
1507       if defined($loanlength) && defined $loanlength->{issuelength};
1508
1509     $sth->execute( '*', $itemtype, '*' );
1510     $loanlength = $sth->fetchrow_hashref;
1511     return $loanlength
1512       if defined($loanlength) && defined $loanlength->{issuelength};
1513
1514     $sth->execute( '*', '*', '*' );
1515     $loanlength = $sth->fetchrow_hashref;
1516     return $loanlength
1517       if defined($loanlength) && defined $loanlength->{issuelength};
1518
1519     # if no rule is set => 0 day (hardcoded)
1520     return {
1521         issuelength => 0,
1522         renewalperiod => 0,
1523         lengthunit => 'days',
1524     };
1525
1526 }
1527
1528
1529 =head2 GetHardDueDate
1530
1531   my ($hardduedate,$hardduedatecompare) = &GetHardDueDate($borrowertype,$itemtype,branchcode)
1532
1533 Get the Hard Due Date and it's comparison for an itemtype, a borrower type and a branch
1534
1535 =cut
1536
1537 sub GetHardDueDate {
1538     my ( $borrowertype, $itemtype, $branchcode ) = @_;
1539
1540     my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
1541         {   categorycode => $borrowertype,
1542             itemtype     => $itemtype,
1543             branchcode   => $branchcode
1544         }
1545     );
1546
1547
1548     if ( defined( $issuing_rule ) ) {
1549         if ( $issuing_rule->hardduedate ) {
1550             return (dt_from_string($issuing_rule->hardduedate, 'iso'),$issuing_rule->hardduedatecompare);
1551         } else {
1552             return (undef, undef);
1553         }
1554     }
1555 }
1556
1557 =head2 GetBranchBorrowerCircRule
1558
1559   my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
1560
1561 Retrieves circulation rule attributes that apply to the given
1562 branch and patron category, regardless of item type.  
1563 The return value is a hashref containing the following key:
1564
1565 maxissueqty - maximum number of loans that a
1566 patron of the given category can have at the given
1567 branch.  If the value is undef, no limit.
1568
1569 maxonsiteissueqty - maximum of on-site checkouts that a
1570 patron of the given category can have at the given
1571 branch.  If the value is undef, no limit.
1572
1573 This will first check for a specific branch and
1574 category match from branch_borrower_circ_rules. 
1575
1576 If no rule is found, it will then check default_branch_circ_rules
1577 (same branch, default category).  If no rule is found,
1578 it will then check default_borrower_circ_rules (default 
1579 branch, same category), then failing that, default_circ_rules
1580 (default branch, default category).
1581
1582 If no rule has been found in the database, it will default to
1583 the buillt in rule:
1584
1585 maxissueqty - undef
1586 maxonsiteissueqty - undef
1587
1588 C<$branchcode> and C<$categorycode> should contain the
1589 literal branch code and patron category code, respectively - no
1590 wildcards.
1591
1592 =cut
1593
1594 sub GetBranchBorrowerCircRule {
1595     my ( $branchcode, $categorycode ) = @_;
1596
1597     my $rules;
1598     my $dbh = C4::Context->dbh();
1599     $rules = $dbh->selectrow_hashref( q|
1600         SELECT maxissueqty, maxonsiteissueqty
1601         FROM branch_borrower_circ_rules
1602         WHERE branchcode = ?
1603         AND   categorycode = ?
1604     |, {}, $branchcode, $categorycode ) ;
1605     return $rules if $rules;
1606
1607     # try same branch, default borrower category
1608     $rules = $dbh->selectrow_hashref( q|
1609         SELECT maxissueqty, maxonsiteissueqty
1610         FROM default_branch_circ_rules
1611         WHERE branchcode = ?
1612     |, {}, $branchcode ) ;
1613     return $rules if $rules;
1614
1615     # try default branch, same borrower category
1616     $rules = $dbh->selectrow_hashref( q|
1617         SELECT maxissueqty, maxonsiteissueqty
1618         FROM default_borrower_circ_rules
1619         WHERE categorycode = ?
1620     |, {}, $categorycode ) ;
1621     return $rules if $rules;
1622
1623     # try default branch, default borrower category
1624     $rules = $dbh->selectrow_hashref( q|
1625         SELECT maxissueqty, maxonsiteissueqty
1626         FROM default_circ_rules
1627     |, {} );
1628     return $rules if $rules;
1629
1630     # built-in default circulation rule
1631     return {
1632         maxissueqty => undef,
1633         maxonsiteissueqty => undef,
1634     };
1635 }
1636
1637 =head2 GetBranchItemRule
1638
1639   my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
1640
1641 Retrieves circulation rule attributes that apply to the given
1642 branch and item type, regardless of patron category.
1643
1644 The return value is a hashref containing the following keys:
1645
1646 holdallowed => Hold policy for this branch and itemtype. Possible values:
1647   0: No holds allowed.
1648   1: Holds allowed only by patrons that have the same homebranch as the item.
1649   2: Holds allowed from any patron.
1650
1651 returnbranch => branch to which to return item.  Possible values:
1652   noreturn: do not return, let item remain where checked in (floating collections)
1653   homebranch: return to item's home branch
1654   holdingbranch: return to issuer branch
1655
1656 This searches branchitemrules in the following order:
1657
1658   * Same branchcode and itemtype
1659   * Same branchcode, itemtype '*'
1660   * branchcode '*', same itemtype
1661   * branchcode and itemtype '*'
1662
1663 Neither C<$branchcode> nor C<$itemtype> should be '*'.
1664
1665 =cut
1666
1667 sub GetBranchItemRule {
1668     my ( $branchcode, $itemtype ) = @_;
1669     my $dbh = C4::Context->dbh();
1670     my $result = {};
1671
1672     my @attempts = (
1673         ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1674             FROM branch_item_rules
1675             WHERE branchcode = ?
1676               AND itemtype = ?', $branchcode, $itemtype],
1677         ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1678             FROM default_branch_circ_rules
1679             WHERE branchcode = ?', $branchcode],
1680         ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1681             FROM default_branch_item_rules
1682             WHERE itemtype = ?', $itemtype],
1683         ['SELECT holdallowed, returnbranch, hold_fulfillment_policy
1684             FROM default_circ_rules'],
1685     );
1686
1687     foreach my $attempt (@attempts) {
1688         my ($query, @bind_params) = @{$attempt};
1689         my $search_result = $dbh->selectrow_hashref ( $query , {}, @bind_params )
1690           or next;
1691
1692         # Since branch/category and branch/itemtype use the same per-branch
1693         # defaults tables, we have to check that the key we want is set, not
1694         # just that a row was returned
1695         $result->{'holdallowed'}  = $search_result->{'holdallowed'}  unless ( defined $result->{'holdallowed'} );
1696         $result->{'hold_fulfillment_policy'} = $search_result->{'hold_fulfillment_policy'} unless ( defined $result->{'hold_fulfillment_policy'} );
1697         $result->{'returnbranch'} = $search_result->{'returnbranch'} unless ( defined $result->{'returnbranch'} );
1698     }
1699     
1700     # built-in default circulation rule
1701     $result->{'holdallowed'} = 2 unless ( defined $result->{'holdallowed'} );
1702     $result->{'hold_fulfillment_policy'} = 'any' unless ( defined $result->{'hold_fulfillment_policy'} );
1703     $result->{'returnbranch'} = 'homebranch' unless ( defined $result->{'returnbranch'} );
1704
1705     return $result;
1706 }
1707
1708 =head2 AddReturn
1709
1710   ($doreturn, $messages, $iteminformation, $borrower) =
1711       &AddReturn( $barcode, $branch [,$exemptfine] [,$dropbox] [,$returndate] );
1712
1713 Returns a book.
1714
1715 =over 4
1716
1717 =item C<$barcode> is the bar code of the book being returned.
1718
1719 =item C<$branch> is the code of the branch where the book is being returned.
1720
1721 =item C<$exemptfine> indicates that overdue charges for the item will be
1722 removed. Optional.
1723
1724 =item C<$dropbox> indicates that the check-in date is assumed to be
1725 yesterday, or the last non-holiday as defined in C4::Calendar .  If
1726 overdue charges are applied and C<$dropbox> is true, the last charge
1727 will be removed.  This assumes that the fines accrual script has run
1728 for _today_. Optional.
1729
1730 =item C<$return_date> allows the default return date to be overridden
1731 by the given return date. Optional.
1732
1733 =back
1734
1735 C<&AddReturn> returns a list of four items:
1736
1737 C<$doreturn> is true iff the return succeeded.
1738
1739 C<$messages> is a reference-to-hash giving feedback on the operation.
1740 The keys of the hash are:
1741
1742 =over 4
1743
1744 =item C<BadBarcode>
1745
1746 No item with this barcode exists. The value is C<$barcode>.
1747
1748 =item C<NotIssued>
1749
1750 The book is not currently on loan. The value is C<$barcode>.
1751
1752 =item C<withdrawn>
1753
1754 This book has been withdrawn/cancelled. The value should be ignored.
1755
1756 =item C<Wrongbranch>
1757
1758 This book has was returned to the wrong branch.  The value is a hashref
1759 so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
1760 contain the branchcode of the incorrect and correct return library, respectively.
1761
1762 =item C<ResFound>
1763
1764 The item was reserved. The value is a reference-to-hash whose keys are
1765 fields from the reserves table of the Koha database, and
1766 C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
1767 either C<Waiting>, C<Reserved>, or 0.
1768
1769 =item C<WasReturned>
1770
1771 Value 1 if return is successful.
1772
1773 =item C<NeedsTransfer>
1774
1775 If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer.
1776
1777 =back
1778
1779 C<$iteminformation> is a reference-to-hash, giving information about the
1780 returned item from the issues table.
1781
1782 C<$borrower> is a reference-to-hash, giving information about the
1783 patron who last borrowed the book.
1784
1785 =cut
1786
1787 sub AddReturn {
1788     my ( $barcode, $branch, $exemptfine, $dropbox, $return_date, $dropboxdate ) = @_;
1789
1790     if ($branch and not Koha::Libraries->find($branch)) {
1791         warn "AddReturn error: branch '$branch' not found.  Reverting to " . C4::Context->userenv->{'branch'};
1792         undef $branch;
1793     }
1794     $branch = C4::Context->userenv->{'branch'} unless $branch;  # we trust userenv to be a safe fallback/default
1795     my $messages;
1796     my $patron;
1797     my $doreturn       = 1;
1798     my $validTransfert = 0;
1799     my $stat_type = 'return';
1800
1801     # get information on item
1802     my $item = GetItem( undef, $barcode );
1803     unless ($item) {
1804         return ( 0, { BadBarcode => $barcode } );    # no barcode means no item or borrower.  bail out.
1805     }
1806
1807     my $itemnumber = $item->{ itemnumber };
1808     my $itemtype = $item->{itype}; # GetItem called effective_itemtype
1809
1810     my $issue  = Koha::Checkouts->find( { itemnumber => $itemnumber } );
1811     if ( $issue ) {
1812         $patron = Koha::Patrons->find( $issue->borrowernumber )
1813             or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existent borrowernumber '" . $issue->borrowernumber . "'\n"
1814                 . Dumper($issue->unblessed) . "\n";
1815     } else {
1816         $messages->{'NotIssued'} = $barcode;
1817         # even though item is not on loan, it may still be transferred;  therefore, get current branch info
1818         $doreturn = 0;
1819         # No issue, no borrowernumber.  ONLY if $doreturn, *might* you have a $borrower later.
1820         # Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
1821         if (C4::Context->preference("RecordLocalUseOnReturn")) {
1822            $messages->{'LocalUse'} = 1;
1823            $stat_type = 'localuse';
1824         }
1825     }
1826
1827     if ( $item->{'location'} eq 'PROC' ) {
1828         if ( C4::Context->preference("InProcessingToShelvingCart") ) {
1829             $item->{'location'} = 'CART';
1830         }
1831         else {
1832             $item->{location} = $item->{permanent_location};
1833         }
1834
1835         ModItem( $item, $item->{'biblionumber'}, $item->{'itemnumber'} );
1836     }
1837
1838         # full item data, but no borrowernumber or checkout info (no issue)
1839     my $hbr = GetBranchItemRule($item->{'homebranch'}, $item->{'itype'})->{'returnbranch'} || "homebranch";
1840         # get the proper branch to which to return the item
1841     my $returnbranch = $item->{$hbr} || $branch ;
1842         # if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
1843
1844     my $borrowernumber = $patron ? $patron->borrowernumber : undef;    # we don't know if we had a borrower or not
1845     my $patron_unblessed = $patron ? $patron->unblessed : {};
1846
1847     my $yaml = C4::Context->preference('UpdateNotForLoanStatusOnCheckin');
1848     if ($yaml) {
1849         $yaml = "$yaml\n\n";  # YAML is anal on ending \n. Surplus does not hurt
1850         my $rules;
1851         eval { $rules = YAML::Load($yaml); };
1852         if ($@) {
1853             warn "Unable to parse UpdateNotForLoanStatusOnCheckin syspref : $@";
1854         }
1855         else {
1856             foreach my $key ( keys %$rules ) {
1857                 if ( $item->{notforloan} eq $key ) {
1858                     $messages->{'NotForLoanStatusUpdated'} = { from => $item->{notforloan}, to => $rules->{$key} };
1859                     ModItem( { notforloan => $rules->{$key} }, undef, $itemnumber );
1860                     last;
1861                 }
1862             }
1863         }
1864     }
1865
1866     # check if the return is allowed at this branch
1867     my ($returnallowed, $message) = CanBookBeReturned($item, $branch);
1868     unless ($returnallowed){
1869         $messages->{'Wrongbranch'} = {
1870             Wrongbranch => $branch,
1871             Rightbranch => $message
1872         };
1873         $doreturn = 0;
1874         return ( $doreturn, $messages, $issue, $patron_unblessed);
1875     }
1876
1877     if ( $item->{'withdrawn'} ) { # book has been cancelled
1878         $messages->{'withdrawn'} = 1;
1879         $doreturn = 0 if C4::Context->preference("BlockReturnOfWithdrawnItems");
1880     }
1881
1882     if ( $item->{itemlost} and C4::Context->preference("BlockReturnOfLostItems") ) {
1883         $doreturn = 0;
1884     }
1885
1886     # case of a return of document (deal with issues and holdingbranch)
1887     my $today = DateTime->now( time_zone => C4::Context->tz() );
1888
1889     if ($doreturn) {
1890         my $is_overdue;
1891         die "The item is not issed and cannot be returned" unless $issue; # Just in case...
1892         $patron or warn "AddReturn without current borrower";
1893                 my $circControlBranch;
1894         if ($dropbox) {
1895             # define circControlBranch only if dropbox mode is set
1896             # don't allow dropbox mode to create an invalid entry in issues (issuedate > today)
1897             # FIXME: check issuedate > returndate, factoring in holidays
1898
1899             $circControlBranch = _GetCircControlBranch($item,$patron_unblessed);
1900             $is_overdue = $issue->is_overdue( $dropboxdate );
1901         } else {
1902             $is_overdue = $issue->is_overdue;
1903         }
1904
1905         if ($patron) {
1906             eval {
1907                 MarkIssueReturned( $borrowernumber, $item->{'itemnumber'},
1908                     $circControlBranch, $return_date, $patron->privacy );
1909             };
1910             unless ( $@ ) {
1911                 if ( ( C4::Context->preference('CalculateFinesOnReturn') && $is_overdue ) || $return_date ) {
1912                     _CalculateAndUpdateFine( { issue => $issue, item => $item, borrower => $patron_unblessed, return_date => $return_date } );
1913                 }
1914             } else {
1915                 carp "The checkin for the following issue failed, Please go to the about page, section 'data corrupted' to know how to fix this problem ($@)" . Dumper( $issue->unblessed );
1916
1917                 return ( 0, { WasReturned => 0, DataCorrupted => 1 }, $issue, $patron_unblessed );
1918             }
1919
1920             # FIXME is the "= 1" right?  This could be the borrower hash.
1921             $messages->{'WasReturned'} = 1;
1922
1923         }
1924
1925         ModItem({ onloan => undef }, $item->{biblionumber}, $item->{'itemnumber'});
1926     }
1927
1928     # the holdingbranch is updated if the document is returned to another location.
1929     # this is always done regardless of whether the item was on loan or not
1930     my $item_holding_branch = $item->{ holdingbranch };
1931     if ($item->{'holdingbranch'} ne $branch) {
1932         UpdateHoldingbranch($branch, $item->{'itemnumber'});
1933         $item->{'holdingbranch'} = $branch; # update item data holdingbranch too
1934     }
1935     ModDateLastSeen( $item->{'itemnumber'} );
1936
1937     # check if we have a transfer for this document
1938     my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->{'itemnumber'} );
1939
1940     # if we have a transfer to do, we update the line of transfers with the datearrived
1941     my $is_in_rotating_collection = C4::RotatingCollections::isItemInAnyCollection( $item->{'itemnumber'} );
1942     if ($datesent) {
1943         if ( $tobranch eq $branch ) {
1944             my $sth = C4::Context->dbh->prepare(
1945                 "UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL"
1946             );
1947             $sth->execute( $item->{'itemnumber'} );
1948             # if we have a reservation with valid transfer, we can set it's status to 'W'
1949             ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
1950             C4::Reserves::ModReserveStatus($item->{'itemnumber'}, 'W');
1951         } else {
1952             $messages->{'WrongTransfer'}     = $tobranch;
1953             $messages->{'WrongTransferItem'} = $item->{'itemnumber'};
1954         }
1955         $validTransfert = 1;
1956     } else {
1957         ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
1958     }
1959
1960     # fix up the accounts.....
1961     if ( $item->{'itemlost'} ) {
1962         $messages->{'WasLost'} = 1;
1963         unless ( C4::Context->preference("BlockReturnOfLostItems") ) {
1964             if (
1965                 Koha::RefundLostItemFeeRules->should_refund(
1966                     {
1967                         current_branch      => C4::Context->userenv->{branch},
1968                         item_home_branch    => $item->{homebranch},
1969                         item_holding_branch => $item_holding_branch
1970                     }
1971                 )
1972               )
1973             {
1974                 _FixAccountForLostAndReturned( $item->{'itemnumber'},
1975                     $borrowernumber, $barcode );
1976                 $messages->{'LostItemFeeRefunded'} = 1;
1977             }
1978         }
1979     }
1980
1981     # fix up the overdues in accounts...
1982     if ($borrowernumber) {
1983         my $fix = _FixOverduesOnReturn($borrowernumber, $item->{itemnumber}, $exemptfine, $dropbox);
1984         defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $item->{itemnumber}...) failed!";  # zero is OK, check defined
1985
1986         if ( $issue and $issue->is_overdue ) {
1987         # fix fine days
1988             $today = $dropboxdate if $dropbox;
1989             my ($debardate,$reminder) = _debar_user_on_return( $patron_unblessed, $item, dt_from_string($issue->date_due), $today );
1990             if ($reminder){
1991                 $messages->{'PrevDebarred'} = $debardate;
1992             } else {
1993                 $messages->{'Debarred'} = $debardate if $debardate;
1994             }
1995         # there's no overdue on the item but borrower had been previously debarred
1996         } elsif ( $issue->date_due and $patron->debarred ) {
1997              if ( $patron->debarred eq "9999-12-31") {
1998                 $messages->{'ForeverDebarred'} = $patron->debarred;
1999              } else {
2000                   my $borrower_debar_dt = dt_from_string( $patron->debarred );
2001                   $borrower_debar_dt->truncate(to => 'day');
2002                   my $today_dt = $today->clone()->truncate(to => 'day');
2003                   if ( DateTime->compare( $borrower_debar_dt, $today_dt ) != -1 ) {
2004                       $messages->{'PrevDebarred'} = $patron->debarred;
2005                   }
2006              }
2007         }
2008     }
2009
2010     # find reserves.....
2011     # if we don't have a reserve with the status W, we launch the Checkreserves routine
2012     my ($resfound, $resrec);
2013     my $lookahead= C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
2014     ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->{'itemnumber'}, undef, $lookahead ) unless ( $item->{'withdrawn'} );
2015     if ($resfound) {
2016           $resrec->{'ResFound'} = $resfound;
2017         $messages->{'ResFound'} = $resrec;
2018     }
2019
2020     # Record the fact that this book was returned.
2021     UpdateStats({
2022         branch         => $branch,
2023         type           => $stat_type,
2024         itemnumber     => $itemnumber,
2025         itemtype       => $itemtype,
2026         borrowernumber => $borrowernumber,
2027         ccode          => $item->{ ccode }
2028     });
2029
2030     # Send a check-in slip. # NOTE: borrower may be undef. Do not try to send messages then.
2031     if ( $patron ) {
2032         my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2033         my %conditions = (
2034             branchcode   => $branch,
2035             categorycode => $patron->categorycode,
2036             item_type    => $item->{itype},
2037             notification => 'CHECKIN',
2038         );
2039         if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
2040             SendCirculationAlert({
2041                 type     => 'CHECKIN',
2042                 item     => $item,
2043                 borrower => $patron->unblessed,
2044                 branch   => $branch,
2045             });
2046         }
2047
2048         logaction("CIRCULATION", "RETURN", $borrowernumber, $item->{'itemnumber'})
2049             if C4::Context->preference("ReturnLog");
2050         }
2051
2052     # Remove any OVERDUES related debarment if the borrower has no overdues
2053     if ( $borrowernumber
2054       && $patron->debarred
2055       && C4::Context->preference('AutoRemoveOverduesRestrictions')
2056       && !Koha::Patrons->find( $borrowernumber )->has_overdues
2057       && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2058     ) {
2059         DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2060     }
2061
2062     # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
2063     if (!$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) and not $messages->{'WrongTransfer'}){
2064         if  (C4::Context->preference("AutomaticItemReturn"    ) or
2065             (C4::Context->preference("UseBranchTransferLimits") and
2066              ! IsBranchTransferAllowed($branch, $returnbranch, $item->{C4::Context->preference("BranchTransferLimitsType")} )
2067            )) {
2068             $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s)", $item->{'itemnumber'},$branch, $returnbranch;
2069             $debug and warn "item: " . Dumper($item);
2070             ModItemTransfer($item->{'itemnumber'}, $branch, $returnbranch);
2071             $messages->{'WasTransfered'} = 1;
2072         } else {
2073             $messages->{'NeedsTransfer'} = $returnbranch;
2074         }
2075     }
2076
2077     return ( $doreturn, $messages, $issue, ( $patron ? $patron->unblessed : {} ));
2078 }
2079
2080 =head2 MarkIssueReturned
2081
2082   MarkIssueReturned($borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy);
2083
2084 Unconditionally marks an issue as being returned by
2085 moving the C<issues> row to C<old_issues> and
2086 setting C<returndate> to the current date, or
2087 the last non-holiday date of the branccode specified in
2088 C<dropbox_branch> .  Assumes you've already checked that 
2089 it's safe to do this, i.e. last non-holiday > issuedate.
2090
2091 if C<$returndate> is specified (in iso format), it is used as the date
2092 of the return. It is ignored when a dropbox_branch is passed in.
2093
2094 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
2095 the old_issue is immediately anonymised
2096
2097 Ideally, this function would be internal to C<C4::Circulation>,
2098 not exported, but it is currently needed by one 
2099 routine in C<C4::Accounts>.
2100
2101 =cut
2102
2103 sub MarkIssueReturned {
2104     my ( $borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy ) = @_;
2105
2106
2107     # Retrieve the issue
2108     my $issue = Koha::Checkouts->find( { itemnumber => $itemnumber } ) or return;
2109     my $issue_id = $issue->issue_id;
2110
2111     my $anonymouspatron;
2112     if ( $privacy == 2 ) {
2113         # The default of 0 will not work due to foreign key constraints
2114         # The anonymisation will fail if AnonymousPatron is not a valid entry
2115         # We need to check if the anonymous patron exist, Koha will fail loudly if it does not
2116         # Note that a warning should appear on the about page (System information tab).
2117         $anonymouspatron = C4::Context->preference('AnonymousPatron');
2118         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."
2119             unless Koha::Patrons->find( $anonymouspatron );
2120     }
2121     my $database = Koha::Database->new();
2122     my $schema   = $database->schema;
2123     my $dbh   = C4::Context->dbh;
2124
2125     my $query = 'UPDATE issues SET returndate=';
2126     my @bind;
2127     if ($dropbox_branch) {
2128         my $calendar = Koha::Calendar->new( branchcode => $dropbox_branch );
2129         my $dropboxdate = $calendar->addDate( DateTime->now( time_zone => C4::Context->tz), -1 );
2130         $query .= ' ? ';
2131         push @bind, $dropboxdate->strftime('%Y-%m-%d %H:%M');
2132     } elsif ($returndate) {
2133         $query .= ' ? ';
2134         push @bind, $returndate;
2135     } else {
2136         $query .= ' now() ';
2137     }
2138     $query .= ' WHERE issue_id = ?';
2139     push @bind, $issue_id;
2140
2141     # FIXME Improve the return value and handle it from callers
2142     $schema->txn_do(sub {
2143
2144         # Update the returndate
2145         $dbh->do( $query, undef, @bind );
2146
2147         # We just updated the returndate, so we need to refetch $issue
2148         $issue->discard_changes;
2149
2150         # Create the old_issues entry
2151         my $old_checkout = Koha::Old::Checkout->new($issue->unblessed)->store;
2152
2153         # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
2154         if ( $privacy == 2) {
2155             $dbh->do(q|UPDATE old_issues SET borrowernumber=? WHERE issue_id = ?|, undef, $anonymouspatron, $old_checkout->issue_id);
2156         }
2157
2158         # And finally delete the issue
2159         $issue->delete;
2160
2161         ModItem( { 'onloan' => undef }, undef, $itemnumber );
2162
2163         if ( C4::Context->preference('StoreLastBorrower') ) {
2164             my $item = Koha::Items->find( $itemnumber );
2165             my $patron = Koha::Patrons->find( $borrowernumber );
2166             $item->last_returned_by( $patron );
2167         }
2168     });
2169
2170     return $issue_id;
2171 }
2172
2173 =head2 _debar_user_on_return
2174
2175     _debar_user_on_return($borrower, $item, $datedue, today);
2176
2177 C<$borrower> borrower hashref
2178
2179 C<$item> item hashref
2180
2181 C<$datedue> date due DateTime object
2182
2183 C<$return_date> DateTime object representing the return time
2184
2185 Internal function, called only by AddReturn that calculates and updates
2186  the user fine days, and debars them if necessary.
2187
2188 Should only be called for overdue returns
2189
2190 =cut
2191
2192 sub _debar_user_on_return {
2193     my ( $borrower, $item, $dt_due, $return_date ) = @_;
2194
2195     my $branchcode = _GetCircControlBranch( $item, $borrower );
2196
2197     my $circcontrol = C4::Context->preference('CircControl');
2198     my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
2199         {   categorycode => $borrower->{categorycode},
2200             itemtype     => $item->{itype},
2201             branchcode   => $branchcode
2202         }
2203     );
2204     my $finedays = $issuing_rule ? $issuing_rule->finedays : undef;
2205     my $unit     = $issuing_rule ? $issuing_rule->lengthunit : undef;
2206     my $chargeable_units = C4::Overdues::get_chargeable_units($unit, $dt_due, $return_date, $branchcode);
2207
2208     if ($finedays) {
2209
2210         # finedays is in days, so hourly loans must multiply by 24
2211         # thus 1 hour late equals 1 day suspension * finedays rate
2212         $finedays = $finedays * 24 if ( $unit eq 'hours' );
2213
2214         # grace period is measured in the same units as the loan
2215         my $grace =
2216           DateTime::Duration->new( $unit => $issuing_rule->firstremind );
2217
2218         my $deltadays = DateTime::Duration->new(
2219             days => $chargeable_units
2220         );
2221         if ( $deltadays->subtract($grace)->is_positive() ) {
2222             my $suspension_days = $deltadays * $finedays;
2223
2224             # If the max suspension days is < than the suspension days
2225             # the suspension days is limited to this maximum period.
2226             my $max_sd = $issuing_rule->maxsuspensiondays;
2227             if ( defined $max_sd ) {
2228                 $max_sd = DateTime::Duration->new( days => $max_sd );
2229                 $suspension_days = $max_sd
2230                   if DateTime::Duration->compare( $max_sd, $suspension_days ) < 0;
2231             }
2232
2233             my ( $has_been_extended, $is_a_reminder );
2234             if ( C4::Context->preference('CumulativeRestrictionPeriods') and $borrower->{debarred} ) {
2235                 my $debarment = @{ GetDebarments( { borrowernumber => $borrower->{borrowernumber}, type => 'SUSPENSION' } ) }[0];
2236                 if ( $debarment ) {
2237                     $return_date = dt_from_string( $debarment->{expiration}, 'sql' );
2238                     $has_been_extended = 1;
2239                 }
2240             }
2241
2242             my $new_debar_dt =
2243               $return_date->clone()->add_duration( $suspension_days );
2244
2245             Koha::Patron::Debarments::AddUniqueDebarment({
2246                 borrowernumber => $borrower->{borrowernumber},
2247                 expiration     => $new_debar_dt->ymd(),
2248                 type           => 'SUSPENSION',
2249             });
2250             # if borrower was already debarred but does not get an extra debarment
2251             my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
2252             my $new_debarment_str;
2253             if ( $borrower->{debarred} eq $patron->is_debarred ) {
2254                 $is_a_reminder = 1;
2255                 $new_debarment_str = $borrower->{debarred};
2256             } else {
2257                 $new_debarment_str = $new_debar_dt->ymd();
2258             }
2259             # FIXME Should return a DateTime object
2260             return $new_debarment_str, $is_a_reminder;
2261         }
2262     }
2263     return;
2264 }
2265
2266 =head2 _FixOverduesOnReturn
2267
2268    &_FixOverduesOnReturn($brn,$itm, $exemptfine, $dropboxmode);
2269
2270 C<$brn> borrowernumber
2271
2272 C<$itm> itemnumber
2273
2274 C<$exemptfine> BOOL -- remove overdue charge associated with this issue. 
2275 C<$dropboxmode> BOOL -- remove lastincrement on overdue charge associated with this issue.
2276
2277 Internal function, called only by AddReturn
2278
2279 =cut
2280
2281 sub _FixOverduesOnReturn {
2282     my ($borrowernumber, $item);
2283     unless ($borrowernumber = shift) {
2284         warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
2285         return;
2286     }
2287     unless ($item = shift) {
2288         warn "_FixOverduesOnReturn() not supplied valid itemnumber";
2289         return;
2290     }
2291     my ($exemptfine, $dropbox) = @_;
2292     my $dbh = C4::Context->dbh;
2293
2294     # check for overdue fine
2295     my $accountline = Koha::Account::Lines->search(
2296         {
2297             borrowernumber => $borrowernumber,
2298             itemnumber     => $item,
2299             -or            => [
2300                 accounttype => 'FU',
2301                 accounttype => 'O',
2302             ],
2303         }
2304     )->next();
2305     return 0 unless $accountline;    # no warning, there's just nothing to fix
2306
2307     my $uquery;
2308     if ($exemptfine) {
2309         my $amountoutstanding = $accountline->amountoutstanding;
2310
2311         $accountline->accounttype('FFOR');
2312         $accountline->amountoutstanding(0);
2313
2314         Koha::Account::Offset->new(
2315             {
2316                 debit_id => $accountline->id,
2317                 type => 'Forgiven',
2318                 amount => $amountoutstanding * -1,
2319             }
2320         );
2321
2322         if (C4::Context->preference("FinesLog")) {
2323             &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
2324         }
2325     } elsif ($dropbox && $accountline->lastincrement) {
2326         my $outstanding = $accountline->amountoutstanding - $accountline->lastincrement;
2327         my $amt = $accountline->amount - $accountline->lastincrement;
2328
2329         Koha::Account::Offset->new(
2330             {
2331                 debit_id => $accountline->id,
2332                 type => 'Dropbox',
2333                 amount => $accountline->lastincrement * -1,
2334             }
2335         );
2336
2337         if ( C4::Context->preference("FinesLog") ) {
2338             &logaction( "FINES", 'MODIFY', $borrowernumber,
2339                 "Dropbox adjustment $amt, item $item" );
2340         }
2341
2342         $accountline->accounttype('F');
2343
2344         if ( $outstanding >= 0 && $amt >= 0 ) {
2345             $accountline->amount($amt);
2346             $accountline->amountoutstanding($outstanding);
2347         }
2348
2349     } else {
2350         $accountline->accounttype('F');
2351     }
2352
2353     return $accountline->store();
2354 }
2355
2356 =head2 _FixAccountForLostAndReturned
2357
2358   &_FixAccountForLostAndReturned($itemnumber, [$borrowernumber, $barcode]);
2359
2360 Calculates the charge for a book lost and returned.
2361
2362 Internal function, not exported, called only by AddReturn.
2363
2364 =cut
2365
2366 sub _FixAccountForLostAndReturned {
2367     my $itemnumber     = shift or return;
2368     my $borrowernumber = @_ ? shift : undef;
2369     my $item_id        = @_ ? shift : $itemnumber;  # Send the barcode if you want that logged in the description
2370
2371     # check for charge made for lost book
2372     my $accountline = Koha::Account::Lines->search(
2373         {
2374             itemnumber  => $itemnumber,
2375             accounttype => { -in => [ 'L', 'Rep', 'W' ] },
2376         },
2377         {
2378             order_by => { -desc => [ 'date', 'accountno' ] }
2379         }
2380     )->next();
2381
2382     return unless $accountline;
2383     return if $accountline->accounttype eq 'W';    # Written off
2384
2385     $accountline->accounttype('LR');
2386     $accountline->store();
2387
2388     my $account = Koha::Account->new( { patron_id => $accountline->borrowernumber } );
2389     my $credit_id = $account->pay(
2390         {
2391             amount       => $accountline->amount,
2392             description  => "Item Returned " . $item_id,
2393             account_type => 'CR',
2394             offset_type  => 'Lost Item Return',
2395             accounlines  => [$accountline],
2396
2397         }
2398     );
2399
2400     ModItem( { paidfor => '' }, undef, $itemnumber );
2401
2402     return $credit_id;
2403 }
2404
2405 =head2 _GetCircControlBranch
2406
2407    my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
2408
2409 Internal function : 
2410
2411 Return the library code to be used to determine which circulation
2412 policy applies to a transaction.  Looks up the CircControl and
2413 HomeOrHoldingBranch system preferences.
2414
2415 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
2416
2417 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
2418
2419 =cut
2420
2421 sub _GetCircControlBranch {
2422     my ($item, $borrower) = @_;
2423     my $circcontrol = C4::Context->preference('CircControl');
2424     my $branch;
2425
2426     if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
2427         $branch= C4::Context->userenv->{'branch'};
2428     } elsif ($circcontrol eq 'PatronLibrary') {
2429         $branch=$borrower->{branchcode};
2430     } else {
2431         my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
2432         $branch = $item->{$branchfield};
2433         # default to item home branch if holdingbranch is used
2434         # and is not defined
2435         if (!defined($branch) && $branchfield eq 'holdingbranch') {
2436             $branch = $item->{homebranch};
2437         }
2438     }
2439     return $branch;
2440 }
2441
2442 =head2 GetOpenIssue
2443
2444   $issue = GetOpenIssue( $itemnumber );
2445
2446 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2447
2448 C<$itemnumber> is the item's itemnumber
2449
2450 Returns a hashref
2451
2452 =cut
2453
2454 sub GetOpenIssue {
2455   my ( $itemnumber ) = @_;
2456   return unless $itemnumber;
2457   my $dbh = C4::Context->dbh;  
2458   my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2459   $sth->execute( $itemnumber );
2460   return $sth->fetchrow_hashref();
2461
2462 }
2463
2464 =head2 GetBiblioIssues
2465
2466   $issues = GetBiblioIssues($biblionumber);
2467
2468 this function get all issues from a biblionumber.
2469
2470 Return:
2471 C<$issues> is a reference to array which each value is ref-to-hash. This ref-to-hash contains all column from
2472 tables issues and the firstname,surname & cardnumber from borrowers.
2473
2474 =cut
2475
2476 sub GetBiblioIssues {
2477     my $biblionumber = shift;
2478     return unless $biblionumber;
2479     my $dbh   = C4::Context->dbh;
2480     my $query = "
2481         SELECT issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2482         FROM issues
2483             LEFT JOIN borrowers ON borrowers.borrowernumber = issues.borrowernumber
2484             LEFT JOIN items ON issues.itemnumber = items.itemnumber
2485             LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2486             LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2487         WHERE biblio.biblionumber = ?
2488         UNION ALL
2489         SELECT old_issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2490         FROM old_issues
2491             LEFT JOIN borrowers ON borrowers.borrowernumber = old_issues.borrowernumber
2492             LEFT JOIN items ON old_issues.itemnumber = items.itemnumber
2493             LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2494             LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2495         WHERE biblio.biblionumber = ?
2496         ORDER BY timestamp
2497     ";
2498     my $sth = $dbh->prepare($query);
2499     $sth->execute($biblionumber, $biblionumber);
2500
2501     my @issues;
2502     while ( my $data = $sth->fetchrow_hashref ) {
2503         push @issues, $data;
2504     }
2505     return \@issues;
2506 }
2507
2508 =head2 GetUpcomingDueIssues
2509
2510   my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2511
2512 =cut
2513
2514 sub GetUpcomingDueIssues {
2515     my $params = shift;
2516
2517     $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2518     my $dbh = C4::Context->dbh;
2519
2520     my $statement = <<END_SQL;
2521 SELECT *
2522 FROM (
2523     SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2524     FROM issues
2525     LEFT JOIN items USING (itemnumber)
2526     LEFT OUTER JOIN branches USING (branchcode)
2527     WHERE returndate is NULL
2528 ) tmp
2529 WHERE days_until_due >= 0 AND days_until_due <= ?
2530 END_SQL
2531
2532     my @bind_parameters = ( $params->{'days_in_advance'} );
2533     
2534     my $sth = $dbh->prepare( $statement );
2535     $sth->execute( @bind_parameters );
2536     my $upcoming_dues = $sth->fetchall_arrayref({});
2537
2538     return $upcoming_dues;
2539 }
2540
2541 =head2 CanBookBeRenewed
2542
2543   ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber[, $override_limit]);
2544
2545 Find out whether a borrowed item may be renewed.
2546
2547 C<$borrowernumber> is the borrower number of the patron who currently
2548 has the item on loan.
2549
2550 C<$itemnumber> is the number of the item to renew.
2551
2552 C<$override_limit>, if supplied with a true value, causes
2553 the limit on the number of times that the loan can be renewed
2554 (as controlled by the item type) to be ignored. Overriding also allows
2555 to renew sooner than "No renewal before" and to manually renew loans
2556 that are automatically renewed.
2557
2558 C<$CanBookBeRenewed> returns a true value if the item may be renewed. The
2559 item must currently be on loan to the specified borrower; renewals
2560 must be allowed for the item's type; and the borrower must not have
2561 already renewed the loan. $error will contain the reason the renewal can not proceed
2562
2563 =cut
2564
2565 sub CanBookBeRenewed {
2566     my ( $borrowernumber, $itemnumber, $override_limit ) = @_;
2567
2568     my $dbh    = C4::Context->dbh;
2569     my $renews = 1;
2570
2571     my $item      = GetItem($itemnumber)      or return ( 0, 'no_item' );
2572     my $issue = Koha::Checkouts->find( { itemnumber => $itemnumber } ) or return ( 0, 'no_checkout' );
2573     return ( 0, 'onsite_checkout' ) if $issue->onsite_checkout;
2574
2575     $borrowernumber ||= $issue->borrowernumber;
2576     my $patron = Koha::Patrons->find( $borrowernumber )
2577       or return;
2578
2579     my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2580
2581     # This item can fill one or more unfilled reserve, can those unfilled reserves
2582     # all be filled by other available items?
2583     if ( $resfound
2584         && C4::Context->preference('AllowRenewalIfOtherItemsAvailable') )
2585     {
2586         my $schema = Koha::Database->new()->schema();
2587
2588         my $item_holds = $schema->resultset('Reserve')->search( { itemnumber => $itemnumber, found => undef } )->count();
2589         if ($item_holds) {
2590             # There is an item level hold on this item, no other item can fill the hold
2591             $resfound = 1;
2592         }
2593         else {
2594
2595             # Get all other items that could possibly fill reserves
2596             my @itemnumbers = $schema->resultset('Item')->search(
2597                 {
2598                     biblionumber => $resrec->{biblionumber},
2599                     onloan       => undef,
2600                     notforloan   => 0,
2601                     -not         => { itemnumber => $itemnumber }
2602                 },
2603                 { columns => 'itemnumber' }
2604             )->get_column('itemnumber')->all();
2605
2606             # Get all other reserves that could have been filled by this item
2607             my @borrowernumbers;
2608             while (1) {
2609                 my ( $reserve_found, $reserve, undef ) =
2610                   C4::Reserves::CheckReserves( $itemnumber, undef, undef, \@borrowernumbers );
2611
2612                 if ($reserve_found) {
2613                     push( @borrowernumbers, $reserve->{borrowernumber} );
2614                 }
2615                 else {
2616                     last;
2617                 }
2618             }
2619
2620             # If the count of the union of the lists of reservable items for each borrower
2621             # is equal or greater than the number of borrowers, we know that all reserves
2622             # can be filled with available items. We can get the union of the sets simply
2623             # by pushing all the elements onto an array and removing the duplicates.
2624             my @reservable;
2625             my %borrowers;
2626             ITEM: foreach my $i (@itemnumbers) {
2627                 my $item = GetItem($i);
2628                 next if IsItemOnHoldAndFound($i);
2629                 for my $b (@borrowernumbers) {
2630                     my $borr = $borrowers{$b} //= Koha::Patrons->find( $b )->unblessed;
2631                     next unless IsAvailableForItemLevelRequest($item, $borr);
2632                     next unless CanItemBeReserved($b,$i);
2633
2634                     push @reservable, $i;
2635                     if (@reservable >= @borrowernumbers) {
2636                         $resfound = 0;
2637                         last ITEM;
2638                     }
2639                     last;
2640                 }
2641             }
2642         }
2643     }
2644     return ( 0, "on_reserve" ) if $resfound;    # '' when no hold was found
2645
2646     return ( 1, undef ) if $override_limit;
2647
2648     my $branchcode = _GetCircControlBranch( $item, $patron->unblessed );
2649     my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
2650         {   categorycode => $patron->categorycode,
2651             itemtype     => $item->{itype},
2652             branchcode   => $branchcode
2653         }
2654     );
2655
2656     return ( 0, "too_many" )
2657       if not $issuing_rule or $issuing_rule->renewalsallowed <= $issue->renewals;
2658
2659     my $overduesblockrenewing = C4::Context->preference('OverduesBlockRenewing');
2660     my $restrictionblockrenewing = C4::Context->preference('RestrictionBlockRenewing');
2661     $patron         = Koha::Patrons->find($borrowernumber); # FIXME Is this really useful?
2662     my $restricted  = $patron->is_debarred;
2663     my $hasoverdues = $patron->has_overdues;
2664
2665     if ( $restricted and $restrictionblockrenewing ) {
2666         return ( 0, 'restriction');
2667     } elsif ( ($hasoverdues and $overduesblockrenewing eq 'block') || ($issue->is_overdue and $overduesblockrenewing eq 'blockitem') ) {
2668         return ( 0, 'overdue');
2669     }
2670
2671     if ( $issue->auto_renew ) {
2672
2673         if ( $patron->category->effective_BlockExpiredPatronOpacActions and $patron->is_expired ) {
2674             return ( 0, 'auto_account_expired' );
2675         }
2676
2677         if ( defined $issuing_rule->no_auto_renewal_after
2678                 and $issuing_rule->no_auto_renewal_after ne "" ) {
2679             # Get issue_date and add no_auto_renewal_after
2680             # If this is greater than today, it's too late for renewal.
2681             my $maximum_renewal_date = dt_from_string($issue->issuedate, 'sql');
2682             $maximum_renewal_date->add(
2683                 $issuing_rule->lengthunit => $issuing_rule->no_auto_renewal_after
2684             );
2685             my $now = dt_from_string;
2686             if ( $now >= $maximum_renewal_date ) {
2687                 return ( 0, "auto_too_late" );
2688             }
2689         }
2690         if ( defined $issuing_rule->no_auto_renewal_after_hard_limit
2691                       and $issuing_rule->no_auto_renewal_after_hard_limit ne "" ) {
2692             # If no_auto_renewal_after_hard_limit is >= today, it's also too late for renewal
2693             if ( dt_from_string >= dt_from_string( $issuing_rule->no_auto_renewal_after_hard_limit ) ) {
2694                 return ( 0, "auto_too_late" );
2695             }
2696         }
2697
2698         if ( C4::Context->preference('OPACFineNoRenewalsBlockAutoRenew') ) {
2699             my $fine_no_renewals = C4::Context->preference("OPACFineNoRenewals");
2700             my $amountoutstanding = $patron->account->balance;
2701             if ( $amountoutstanding and $amountoutstanding > $fine_no_renewals ) {
2702                 return ( 0, "auto_too_much_oweing" );
2703             }
2704         }
2705     }
2706
2707     if ( defined $issuing_rule->norenewalbefore
2708         and $issuing_rule->norenewalbefore ne "" )
2709     {
2710
2711         # Calculate soonest renewal by subtracting 'No renewal before' from due date
2712         my $soonestrenewal = dt_from_string( $issue->date_due, 'sql' )->subtract(
2713             $issuing_rule->lengthunit => $issuing_rule->norenewalbefore );
2714
2715         # Depending on syspref reset the exact time, only check the date
2716         if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
2717             and $issuing_rule->lengthunit eq 'days' )
2718         {
2719             $soonestrenewal->truncate( to => 'day' );
2720         }
2721
2722         if ( $soonestrenewal > DateTime->now( time_zone => C4::Context->tz() ) )
2723         {
2724             return ( 0, "auto_too_soon" ) if $issue->auto_renew;
2725             return ( 0, "too_soon" );
2726         }
2727         elsif ( $issue->auto_renew ) {
2728             return ( 0, "auto_renew" );
2729         }
2730     }
2731
2732     # Fallback for automatic renewals:
2733     # If norenewalbefore is undef, don't renew before due date.
2734     if ( $issue->auto_renew ) {
2735         my $now = dt_from_string;
2736         return ( 0, "auto_renew" )
2737           if $now >= dt_from_string( $issue->date_due, 'sql' );
2738         return ( 0, "auto_too_soon" );
2739     }
2740
2741     return ( 1, undef );
2742 }
2743
2744 =head2 AddRenewal
2745
2746   &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
2747
2748 Renews a loan.
2749
2750 C<$borrowernumber> is the borrower number of the patron who currently
2751 has the item.
2752
2753 C<$itemnumber> is the number of the item to renew.
2754
2755 C<$branch> is the library where the renewal took place (if any).
2756            The library that controls the circ policies for the renewal is retrieved from the issues record.
2757
2758 C<$datedue> can be a DateTime object used to set the due date.
2759
2760 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate.  If
2761 this parameter is not supplied, lastreneweddate is set to the current date.
2762
2763 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2764 from the book's item type.
2765
2766 =cut
2767
2768 sub AddRenewal {
2769     my $borrowernumber  = shift;
2770     my $itemnumber      = shift or return;
2771     my $branch          = shift;
2772     my $datedue         = shift;
2773     my $lastreneweddate = shift || DateTime->now(time_zone => C4::Context->tz)->ymd();
2774
2775     my $item   = GetItem($itemnumber) or return;
2776     my $item_object = Koha::Items->find( $itemnumber ); # Should replace $item
2777     my $biblio = $item_object->biblio;
2778
2779     my $dbh = C4::Context->dbh;
2780
2781     # Find the issues record for this book
2782     my $issue = Koha::Checkouts->find( { itemnumber => $itemnumber } );
2783
2784     return unless $issue;
2785
2786     $borrowernumber ||= $issue->borrowernumber;
2787
2788     if ( defined $datedue && ref $datedue ne 'DateTime' ) {
2789         carp 'Invalid date passed to AddRenewal.';
2790         return;
2791     }
2792
2793     my $patron = Koha::Patrons->find( $borrowernumber ) or return; # FIXME Should do more than just return
2794     my $patron_unblessed = $patron->unblessed;
2795
2796     if ( C4::Context->preference('CalculateFinesOnReturn') && $issue->is_overdue ) {
2797         _CalculateAndUpdateFine( { issue => $issue, item => $item, borrower => $patron_unblessed } );
2798     }
2799     _FixOverduesOnReturn( $borrowernumber, $itemnumber );
2800
2801     # If the due date wasn't specified, calculate it by adding the
2802     # book's loan length to today's date or the current due date
2803     # based on the value of the RenewalPeriodBase syspref.
2804     unless ($datedue) {
2805
2806         my $itemtype = $item_object->effective_itemtype;
2807         $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
2808                                         dt_from_string( $issue->date_due, 'sql' ) :
2809                                         DateTime->now( time_zone => C4::Context->tz());
2810         $datedue =  CalcDateDue($datedue, $itemtype, _GetCircControlBranch($item, $patron_unblessed), $patron_unblessed, 'is a renewal');
2811     }
2812
2813     # Update the issues record to have the new due date, and a new count
2814     # of how many times it has been renewed.
2815     my $renews = $issue->renewals + 1;
2816     my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
2817                             WHERE borrowernumber=? 
2818                             AND itemnumber=?"
2819     );
2820
2821     $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
2822
2823     # Update the renewal count on the item, and tell zebra to reindex
2824     $renews = $item->{renewals} + 1;
2825     ModItem({ renewals => $renews, onloan => $datedue->strftime('%Y-%m-%d %H:%M')}, $item->{biblionumber}, $itemnumber);
2826
2827     # Charge a new rental fee, if applicable?
2828     my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
2829     if ( $charge > 0 ) {
2830         my $accountno = getnextacctno( $borrowernumber );
2831         my $manager_id = 0;
2832         $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv; 
2833         $sth = $dbh->prepare(
2834                 "INSERT INTO accountlines
2835                     (date, borrowernumber, accountno, amount, manager_id,
2836                     description,accounttype, amountoutstanding, itemnumber)
2837                     VALUES (now(),?,?,?,?,?,?,?,?)"
2838         );
2839         $sth->execute( $borrowernumber, $accountno, $charge, $manager_id,
2840             "Renewal of Rental Item " . $biblio->title . " $item->{'barcode'}",
2841             'Rent', $charge, $itemnumber );
2842     }
2843
2844     # Send a renewal slip according to checkout alert preferencei
2845     if ( C4::Context->preference('RenewalSendNotice') eq '1' ) {
2846         my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2847         my %conditions        = (
2848             branchcode   => $branch,
2849             categorycode => $patron->categorycode,
2850             item_type    => $item->{itype},
2851             notification => 'CHECKOUT',
2852         );
2853         if ( $circulation_alert->is_enabled_for( \%conditions ) ) {
2854             SendCirculationAlert(
2855                 {
2856                     type     => 'RENEWAL',
2857                     item     => $item,
2858                     borrower => $patron->unblessed,
2859                     branch   => $branch,
2860                 }
2861             );
2862         }
2863     }
2864
2865     # Remove any OVERDUES related debarment if the borrower has no overdues
2866     if ( $patron
2867       && $patron->is_debarred
2868       && ! $patron->has_overdues
2869       && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2870     ) {
2871         DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2872     }
2873
2874     unless ( C4::Context->interface eq 'opac' ) { #if from opac we are obeying OpacRenewalBranch as calculated in opac-renew.pl
2875         $branch = C4::Context->userenv ? C4::Context->userenv->{branch} : $branch;
2876     }
2877
2878     # Add the renewal to stats
2879     UpdateStats(
2880         {
2881             branch         => $branch,
2882             type           => 'renew',
2883             amount         => $charge,
2884             itemnumber     => $itemnumber,
2885             itemtype       => $item->{itype},
2886             location       => $item->{location},
2887             borrowernumber => $borrowernumber,
2888             ccode          => $item->{'ccode'}
2889         }
2890     );
2891
2892     #Log the renewal
2893     logaction("CIRCULATION", "RENEWAL", $borrowernumber, $itemnumber) if C4::Context->preference("RenewalLog");
2894     return $datedue;
2895 }
2896
2897 sub GetRenewCount {
2898     # check renewal status
2899     my ( $bornum, $itemno ) = @_;
2900     my $dbh           = C4::Context->dbh;
2901     my $renewcount    = 0;
2902     my $renewsallowed = 0;
2903     my $renewsleft    = 0;
2904
2905     my $patron = Koha::Patrons->find( $bornum );
2906     my $item     = GetItem($itemno);
2907
2908     return (0, 0, 0) unless $patron or $item; # Wrong call, no renewal allowed
2909
2910     # Look in the issues table for this item, lent to this borrower,
2911     # and not yet returned.
2912
2913     # FIXME - I think this function could be redone to use only one SQL call.
2914     my $sth = $dbh->prepare(
2915         "select * from issues
2916                                 where (borrowernumber = ?)
2917                                 and (itemnumber = ?)"
2918     );
2919     $sth->execute( $bornum, $itemno );
2920     my $data = $sth->fetchrow_hashref;
2921     $renewcount = $data->{'renewals'} if $data->{'renewals'};
2922     # $item and $borrower should be calculated
2923     my $branchcode = _GetCircControlBranch($item, $patron->unblessed);
2924
2925     my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
2926         {   categorycode => $patron->categorycode,
2927             itemtype     => $item->{itype},
2928             branchcode   => $branchcode
2929         }
2930     );
2931
2932     $renewsallowed = $issuing_rule ? $issuing_rule->renewalsallowed : 0;
2933     $renewsleft    = $renewsallowed - $renewcount;
2934     if($renewsleft < 0){ $renewsleft = 0; }
2935     return ( $renewcount, $renewsallowed, $renewsleft );
2936 }
2937
2938 =head2 GetSoonestRenewDate
2939
2940   $NoRenewalBeforeThisDate = &GetSoonestRenewDate($borrowernumber, $itemnumber);
2941
2942 Find out the soonest possible renew date of a borrowed item.
2943
2944 C<$borrowernumber> is the borrower number of the patron who currently
2945 has the item on loan.
2946
2947 C<$itemnumber> is the number of the item to renew.
2948
2949 C<$GetSoonestRenewDate> returns the DateTime of the soonest possible
2950 renew date, based on the value "No renewal before" of the applicable
2951 issuing rule. Returns the current date if the item can already be
2952 renewed, and returns undefined if the borrower, loan, or item
2953 cannot be found.
2954
2955 =cut
2956
2957 sub GetSoonestRenewDate {
2958     my ( $borrowernumber, $itemnumber ) = @_;
2959
2960     my $dbh = C4::Context->dbh;
2961
2962     my $item      = GetItem($itemnumber)      or return;
2963     my $itemissue = Koha::Checkouts->find( { itemnumber => $itemnumber } ) or return;
2964
2965     $borrowernumber ||= $itemissue->borrowernumber;
2966     my $patron = Koha::Patrons->find( $borrowernumber )
2967       or return;
2968
2969     my $branchcode = _GetCircControlBranch( $item, $patron->unblessed );
2970     my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
2971         {   categorycode => $patron->categorycode,
2972             itemtype     => $item->{itype},
2973             branchcode   => $branchcode
2974         }
2975     );
2976
2977     my $now = dt_from_string;
2978     return $now unless $issuing_rule;
2979
2980     if ( defined $issuing_rule->norenewalbefore
2981         and $issuing_rule->norenewalbefore ne "" )
2982     {
2983         my $soonestrenewal =
2984           dt_from_string( $itemissue->date_due )->subtract(
2985             $issuing_rule->lengthunit => $issuing_rule->norenewalbefore );
2986
2987         if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date'
2988             and $issuing_rule->lengthunit eq 'days' )
2989         {
2990             $soonestrenewal->truncate( to => 'day' );
2991         }
2992         return $soonestrenewal if $now < $soonestrenewal;
2993     }
2994     return $now;
2995 }
2996
2997 =head2 GetLatestAutoRenewDate
2998
2999   $NoAutoRenewalAfterThisDate = &GetLatestAutoRenewDate($borrowernumber, $itemnumber);
3000
3001 Find out the latest possible auto renew date of a borrowed item.
3002
3003 C<$borrowernumber> is the borrower number of the patron who currently
3004 has the item on loan.
3005
3006 C<$itemnumber> is the number of the item to renew.
3007
3008 C<$GetLatestAutoRenewDate> returns the DateTime of the latest possible
3009 auto renew date, based on the value "No auto renewal after" and the "No auto
3010 renewal after (hard limit) of the applicable issuing rule.
3011 Returns undef if there is no date specify in the circ rules or if the patron, loan,
3012 or item cannot be found.
3013
3014 =cut
3015
3016 sub GetLatestAutoRenewDate {
3017     my ( $borrowernumber, $itemnumber ) = @_;
3018
3019     my $dbh = C4::Context->dbh;
3020
3021     my $item      = GetItem($itemnumber)      or return;
3022     my $itemissue = Koha::Checkouts->find( { itemnumber => $itemnumber } ) or return;
3023
3024     $borrowernumber ||= $itemissue->borrowernumber;
3025     my $patron = Koha::Patrons->find( $borrowernumber )
3026       or return;
3027
3028     my $branchcode = _GetCircControlBranch( $item, $patron->unblessed );
3029     my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule(
3030         {   categorycode => $patron->categorycode,
3031             itemtype     => $item->{itype},
3032             branchcode   => $branchcode
3033         }
3034     );
3035
3036     return unless $issuing_rule;
3037     return
3038       if ( not $issuing_rule->no_auto_renewal_after
3039             or $issuing_rule->no_auto_renewal_after eq '' )
3040       and ( not $issuing_rule->no_auto_renewal_after_hard_limit
3041              or $issuing_rule->no_auto_renewal_after_hard_limit eq '' );
3042
3043     my $maximum_renewal_date;
3044     if ( $issuing_rule->no_auto_renewal_after ) {
3045         $maximum_renewal_date = dt_from_string($itemissue->issuedate);
3046         $maximum_renewal_date->add(
3047             $issuing_rule->lengthunit => $issuing_rule->no_auto_renewal_after
3048         );
3049     }
3050
3051     if ( $issuing_rule->no_auto_renewal_after_hard_limit ) {
3052         my $dt = dt_from_string( $issuing_rule->no_auto_renewal_after_hard_limit );
3053         $maximum_renewal_date = $dt if not $maximum_renewal_date or $maximum_renewal_date > $dt;
3054     }
3055     return $maximum_renewal_date;
3056 }
3057
3058
3059 =head2 GetIssuingCharges
3060
3061   ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
3062
3063 Calculate how much it would cost for a given patron to borrow a given
3064 item, including any applicable discounts.
3065
3066 C<$itemnumber> is the item number of item the patron wishes to borrow.
3067
3068 C<$borrowernumber> is the patron's borrower number.
3069
3070 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
3071 and C<$item_type> is the code for the item's item type (e.g., C<VID>
3072 if it's a video).
3073
3074 =cut
3075
3076 sub GetIssuingCharges {
3077
3078     # calculate charges due
3079     my ( $itemnumber, $borrowernumber ) = @_;
3080     my $charge = 0;
3081     my $dbh    = C4::Context->dbh;
3082     my $item_type;
3083
3084     # Get the book's item type and rental charge (via its biblioitem).
3085     my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
3086         LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
3087     $charge_query .= (C4::Context->preference('item-level_itypes'))
3088         ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
3089         : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
3090
3091     $charge_query .= ' WHERE items.itemnumber =?';
3092
3093     my $sth = $dbh->prepare($charge_query);
3094     $sth->execute($itemnumber);
3095     if ( my $item_data = $sth->fetchrow_hashref ) {
3096         $item_type = $item_data->{itemtype};
3097         $charge    = $item_data->{rentalcharge};
3098         my $branch = C4::Context::mybranch();
3099         my $discount_query = q|SELECT rentaldiscount,
3100             issuingrules.itemtype, issuingrules.branchcode
3101             FROM borrowers
3102             LEFT JOIN issuingrules ON borrowers.categorycode = issuingrules.categorycode
3103             WHERE borrowers.borrowernumber = ?
3104             AND (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
3105             AND (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')|;
3106         my $discount_sth = $dbh->prepare($discount_query);
3107         $discount_sth->execute( $borrowernumber, $item_type, $branch );
3108         my $discount_rules = $discount_sth->fetchall_arrayref({});
3109         if (@{$discount_rules}) {
3110             # We may have multiple rules so get the most specific
3111             my $discount = _get_discount_from_rule($discount_rules, $branch, $item_type);
3112             $charge = ( $charge * ( 100 - $discount ) ) / 100;
3113         }
3114         if ($charge) {
3115             $charge = sprintf '%.2f', $charge; # ensure no fractions of a penny returned
3116         }
3117     }
3118
3119     return ( $charge, $item_type );
3120 }
3121
3122 # Select most appropriate discount rule from those returned
3123 sub _get_discount_from_rule {
3124     my ($rules_ref, $branch, $itemtype) = @_;
3125     my $discount;
3126
3127     if (@{$rules_ref} == 1) { # only 1 applicable rule use it
3128         $discount = $rules_ref->[0]->{rentaldiscount};
3129         return (defined $discount) ? $discount : 0;
3130     }
3131     # could have up to 4 does one match $branch and $itemtype
3132     my @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq $itemtype } @{$rules_ref};
3133     if (@d) {
3134         $discount = $d[0]->{rentaldiscount};
3135         return (defined $discount) ? $discount : 0;
3136     }
3137     # do we have item type + all branches
3138     @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq $itemtype } @{$rules_ref};
3139     if (@d) {
3140         $discount = $d[0]->{rentaldiscount};
3141         return (defined $discount) ? $discount : 0;
3142     }
3143     # do we all item types + this branch
3144     @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq q{*} } @{$rules_ref};
3145     if (@d) {
3146         $discount = $d[0]->{rentaldiscount};
3147         return (defined $discount) ? $discount : 0;
3148     }
3149     # so all and all (surely we wont get here)
3150     @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq q{*} } @{$rules_ref};
3151     if (@d) {
3152         $discount = $d[0]->{rentaldiscount};
3153         return (defined $discount) ? $discount : 0;
3154     }
3155     # none of the above
3156     return 0;
3157 }
3158
3159 =head2 AddIssuingCharge
3160
3161   &AddIssuingCharge( $itemno, $borrowernumber, $charge )
3162
3163 =cut
3164
3165 sub AddIssuingCharge {
3166     my ( $itemnumber, $borrowernumber, $charge ) = @_;
3167
3168     my $nextaccntno = getnextacctno($borrowernumber);
3169
3170     my $manager_id  = 0;
3171     $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
3172
3173     my $accountline = Koha::Account::Line->new(
3174         {
3175             borrowernumber    => $borrowernumber,
3176             itemnumber        => $itemnumber,
3177             accountno         => $nextaccntno,
3178             amount            => $charge,
3179             amountoutstanding => $charge,
3180             manager_id        => $manager_id,
3181             description       => 'Rental',
3182             accounttype       => 'Rent',
3183             date              => \'NOW()',
3184         }
3185     )->store();
3186
3187     Koha::Account::Offset->new(
3188         {
3189             debit_id => $accountline->id,
3190             type     => 'Rental Fee',
3191             amount   => $charge,
3192         }
3193     )->store();
3194 }
3195
3196 =head2 GetTransfers
3197
3198   GetTransfers($itemnumber);
3199
3200 =cut
3201
3202 sub GetTransfers {
3203     my ($itemnumber) = @_;
3204
3205     my $dbh = C4::Context->dbh;
3206
3207     my $query = '
3208         SELECT datesent,
3209                frombranch,
3210                tobranch,
3211                branchtransfer_id
3212         FROM branchtransfers
3213         WHERE itemnumber = ?
3214           AND datearrived IS NULL
3215         ';
3216     my $sth = $dbh->prepare($query);
3217     $sth->execute($itemnumber);
3218     my @row = $sth->fetchrow_array();
3219     return @row;
3220 }
3221
3222 =head2 GetTransfersFromTo
3223
3224   @results = GetTransfersFromTo($frombranch,$tobranch);
3225
3226 Returns the list of pending transfers between $from and $to branch
3227
3228 =cut
3229
3230 sub GetTransfersFromTo {
3231     my ( $frombranch, $tobranch ) = @_;
3232     return unless ( $frombranch && $tobranch );
3233     my $dbh   = C4::Context->dbh;
3234     my $query = "
3235         SELECT branchtransfer_id,itemnumber,datesent,frombranch
3236         FROM   branchtransfers
3237         WHERE  frombranch=?
3238           AND  tobranch=?
3239           AND datearrived IS NULL
3240     ";
3241     my $sth = $dbh->prepare($query);
3242     $sth->execute( $frombranch, $tobranch );
3243     my @gettransfers;
3244
3245     while ( my $data = $sth->fetchrow_hashref ) {
3246         push @gettransfers, $data;
3247     }
3248     return (@gettransfers);
3249 }
3250
3251 =head2 DeleteTransfer
3252
3253   &DeleteTransfer($itemnumber);
3254
3255 =cut
3256
3257 sub DeleteTransfer {
3258     my ($itemnumber) = @_;
3259     return unless $itemnumber;
3260     my $dbh          = C4::Context->dbh;
3261     my $sth          = $dbh->prepare(
3262         "DELETE FROM branchtransfers
3263          WHERE itemnumber=?
3264          AND datearrived IS NULL "
3265     );
3266     return $sth->execute($itemnumber);
3267 }
3268
3269 =head2 SendCirculationAlert
3270
3271 Send out a C<check-in> or C<checkout> alert using the messaging system.
3272
3273 B<Parameters>:
3274
3275 =over 4
3276
3277 =item type
3278
3279 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
3280
3281 =item item
3282
3283 Hashref of information about the item being checked in or out.
3284
3285 =item borrower
3286
3287 Hashref of information about the borrower of the item.
3288
3289 =item branch
3290
3291 The branchcode from where the checkout or check-in took place.
3292
3293 =back
3294
3295 B<Example>:
3296
3297     SendCirculationAlert({
3298         type     => 'CHECKOUT',
3299         item     => $item,
3300         borrower => $borrower,
3301         branch   => $branch,
3302     });
3303
3304 =cut
3305
3306 sub SendCirculationAlert {
3307     my ($opts) = @_;
3308     my ($type, $item, $borrower, $branch) =
3309         ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
3310     my %message_name = (
3311         CHECKIN  => 'Item_Check_in',
3312         CHECKOUT => 'Item_Checkout',
3313         RENEWAL  => 'Item_Checkout',
3314     );
3315     my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
3316         borrowernumber => $borrower->{borrowernumber},
3317         message_name   => $message_name{$type},
3318     });
3319     my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues';
3320
3321     my $schema = Koha::Database->new->schema;
3322     my @transports = keys %{ $borrower_preferences->{transports} };
3323
3324     # From the MySQL doc:
3325     # LOCK TABLES is not transaction-safe and implicitly commits any active transaction before attempting to lock the tables.
3326     # If the LOCK/UNLOCK statements are executed from tests, the current transaction will be committed.
3327     # To avoid that we need to guess if this code is execute from tests or not (yes it is a bit hacky)
3328     my $do_not_lock = ( exists $ENV{_} && $ENV{_} =~ m|prove| ) || $ENV{KOHA_NO_TABLE_LOCKS};
3329
3330     for my $mtt (@transports) {
3331         my $letter =  C4::Letters::GetPreparedLetter (
3332             module => 'circulation',
3333             letter_code => $type,
3334             branchcode => $branch,
3335             message_transport_type => $mtt,
3336             lang => $borrower->{lang},
3337             tables => {
3338                 $issues_table => $item->{itemnumber},
3339                 'items'       => $item->{itemnumber},
3340                 'biblio'      => $item->{biblionumber},
3341                 'biblioitems' => $item->{biblionumber},
3342                 'borrowers'   => $borrower,
3343                 'branches'    => $branch,
3344             }
3345         ) or next;
3346
3347         $schema->storage->txn_begin;
3348         C4::Context->dbh->do(q|LOCK TABLE message_queue READ|) unless $do_not_lock;
3349         C4::Context->dbh->do(q|LOCK TABLE message_queue WRITE|) unless $do_not_lock;
3350         my $message = C4::Message->find_last_message($borrower, $type, $mtt);
3351         unless ( $message ) {
3352             C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3353             C4::Message->enqueue($letter, $borrower, $mtt);
3354         } else {
3355             $message->append($letter);
3356             $message->update;
3357         }
3358         C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock;
3359         $schema->storage->txn_commit;
3360     }
3361
3362     return;
3363 }
3364
3365 =head2 updateWrongTransfer
3366
3367   $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
3368
3369 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 
3370
3371 =cut
3372
3373 sub updateWrongTransfer {
3374         my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
3375         my $dbh = C4::Context->dbh;     
3376 # first step validate the actual line of transfert .
3377         my $sth =
3378                 $dbh->prepare(
3379                         "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
3380                 );
3381                 $sth->execute($FromLibrary,$itemNumber);
3382
3383 # second step create a new line of branchtransfer to the right location .
3384         ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
3385
3386 #third step changing holdingbranch of item
3387         UpdateHoldingbranch($FromLibrary,$itemNumber);
3388 }
3389
3390 =head2 UpdateHoldingbranch
3391
3392   $items = UpdateHoldingbranch($branch,$itmenumber);
3393
3394 Simple methode for updating hodlingbranch in items BDD line
3395
3396 =cut
3397
3398 sub UpdateHoldingbranch {
3399         my ( $branch,$itemnumber ) = @_;
3400     ModItem({ holdingbranch => $branch }, undef, $itemnumber);
3401 }
3402
3403 =head2 CalcDateDue
3404
3405 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
3406
3407 this function calculates the due date given the start date and configured circulation rules,
3408 checking against the holidays calendar as per the 'useDaysMode' syspref.
3409 C<$startdate>   = DateTime object representing start date of loan period (assumed to be today)
3410 C<$itemtype>  = itemtype code of item in question
3411 C<$branch>  = location whose calendar to use
3412 C<$borrower> = Borrower object
3413 C<$isrenewal> = Boolean: is true if we want to calculate the date due for a renewal. Else is false.
3414
3415 =cut
3416
3417 sub CalcDateDue {
3418     my ( $startdate, $itemtype, $branch, $borrower, $isrenewal ) = @_;
3419
3420     $isrenewal ||= 0;
3421
3422     # loanlength now a href
3423     my $loanlength =
3424             GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
3425
3426     my $length_key = ( $isrenewal and defined $loanlength->{renewalperiod} )
3427             ? qq{renewalperiod}
3428             : qq{issuelength};
3429
3430     my $datedue;
3431     if ( $startdate ) {
3432         if (ref $startdate ne 'DateTime' ) {
3433             $datedue = dt_from_string($datedue);
3434         } else {
3435             $datedue = $startdate->clone;
3436         }
3437     } else {
3438         $datedue =
3439           DateTime->now( time_zone => C4::Context->tz() )
3440           ->truncate( to => 'minute' );
3441     }
3442
3443
3444     # calculate the datedue as normal
3445     if ( C4::Context->preference('useDaysMode') eq 'Days' )
3446     {    # ignoring calendar
3447         if ( $loanlength->{lengthunit} eq 'hours' ) {
3448             $datedue->add( hours => $loanlength->{$length_key} );
3449         } else {    # days
3450             $datedue->add( days => $loanlength->{$length_key} );
3451             $datedue->set_hour(23);
3452             $datedue->set_minute(59);
3453         }
3454     } else {
3455         my $dur;
3456         if ($loanlength->{lengthunit} eq 'hours') {
3457             $dur = DateTime::Duration->new( hours => $loanlength->{$length_key});
3458         }
3459         else { # days
3460             $dur = DateTime::Duration->new( days => $loanlength->{$length_key});
3461         }
3462         my $calendar = Koha::Calendar->new( branchcode => $branch );
3463         $datedue = $calendar->addDate( $datedue, $dur, $loanlength->{lengthunit} );
3464         if ($loanlength->{lengthunit} eq 'days') {
3465             $datedue->set_hour(23);
3466             $datedue->set_minute(59);
3467         }
3468     }
3469
3470     # if Hard Due Dates are used, retrieve them and apply as necessary
3471     my ( $hardduedate, $hardduedatecompare ) =
3472       GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
3473     if ($hardduedate) {    # hardduedates are currently dates
3474         $hardduedate->truncate( to => 'minute' );
3475         $hardduedate->set_hour(23);
3476         $hardduedate->set_minute(59);
3477         my $cmp = DateTime->compare( $hardduedate, $datedue );
3478
3479 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
3480 # if the calculated date is before the 'after' Hard Due Date (floor), override
3481 # if the hard due date is set to 'exactly', overrride
3482         if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
3483             $datedue = $hardduedate->clone;
3484         }
3485
3486         # in all other cases, keep the date due as it is
3487
3488     }
3489
3490     # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3491     if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3492         my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso', 'floating');
3493         if( $expiry_dt ) { #skip empty expiry date..
3494             $expiry_dt->set( hour => 23, minute => 59);
3495             my $d1= $datedue->clone->set_time_zone('floating');
3496             if ( DateTime->compare( $d1, $expiry_dt ) == 1 ) {
3497                 $datedue = $expiry_dt->clone->set_time_zone( C4::Context->tz );
3498             }
3499         }
3500         if ( C4::Context->preference('useDaysMode') ne 'Days' ) {
3501           my $calendar = Koha::Calendar->new( branchcode => $branch );
3502           if ( $calendar->is_holiday($datedue) ) {
3503               # Don't return on a closed day
3504               $datedue = $calendar->prev_open_day( $datedue );
3505           }
3506         }
3507     }
3508
3509     return $datedue;
3510 }
3511
3512
3513 sub CheckValidBarcode{
3514 my ($barcode) = @_;
3515 my $dbh = C4::Context->dbh;
3516 my $query=qq|SELECT count(*) 
3517              FROM items 
3518              WHERE barcode=?
3519             |;
3520 my $sth = $dbh->prepare($query);
3521 $sth->execute($barcode);
3522 my $exist=$sth->fetchrow ;
3523 return $exist;
3524 }
3525
3526 =head2 IsBranchTransferAllowed
3527
3528   $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3529
3530 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3531
3532 =cut
3533
3534 sub IsBranchTransferAllowed {
3535         my ( $toBranch, $fromBranch, $code ) = @_;
3536
3537         if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3538         
3539         my $limitType = C4::Context->preference("BranchTransferLimitsType");   
3540         my $dbh = C4::Context->dbh;
3541             
3542         my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3543         $sth->execute( $toBranch, $fromBranch, $code );
3544         my $limit = $sth->fetchrow_hashref();
3545                         
3546         ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3547         if ( $limit->{'limitId'} ) {
3548                 return 0;
3549         } else {
3550                 return 1;
3551         }
3552 }                                                        
3553
3554 =head2 CreateBranchTransferLimit
3555
3556   CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3557
3558 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3559
3560 =cut
3561
3562 sub CreateBranchTransferLimit {
3563    my ( $toBranch, $fromBranch, $code ) = @_;
3564    return unless defined($toBranch) && defined($fromBranch);
3565    my $limitType = C4::Context->preference("BranchTransferLimitsType");
3566    
3567    my $dbh = C4::Context->dbh;
3568    
3569    my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3570    return $sth->execute( $code, $toBranch, $fromBranch );
3571 }
3572
3573 =head2 DeleteBranchTransferLimits
3574
3575     my $result = DeleteBranchTransferLimits($frombranch);
3576
3577 Deletes all the library transfer limits for one library.  Returns the
3578 number of limits deleted, 0e0 if no limits were deleted, or undef if
3579 no arguments are supplied.
3580
3581 =cut
3582
3583 sub DeleteBranchTransferLimits {
3584     my $branch = shift;
3585     return unless defined $branch;
3586     my $dbh    = C4::Context->dbh;
3587     my $sth    = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3588     return $sth->execute($branch);
3589 }
3590
3591 sub ReturnLostItem{
3592     my ( $borrowernumber, $itemnum ) = @_;
3593
3594     MarkIssueReturned( $borrowernumber, $itemnum );
3595     my $patron = Koha::Patrons->find( $borrowernumber );
3596     my $item = C4::Items::GetItem( $itemnum );
3597     my $old_note = ($item->{'paidfor'} && ($item->{'paidfor'} ne q{})) ? $item->{'paidfor'}.' / ' : q{};
3598     my @datearr = localtime(time);
3599     my $date = ( 1900 + $datearr[5] ) . "-" . ( $datearr[4] + 1 ) . "-" . $datearr[3];
3600     my $bor = $patron->firstname . ' ' . $patron->surname . ' ' . $patron->cardnumber;
3601     ModItem({ paidfor =>  $old_note."Paid for by $bor $date" }, undef, $itemnum);
3602 }
3603
3604
3605 sub LostItem{
3606     my ($itemnumber, $mark_returned) = @_;
3607
3608     $mark_returned //= C4::Context->preference('MarkLostItemsAsReturned');
3609
3610     my $dbh = C4::Context->dbh();
3611     my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title 
3612                            FROM issues 
3613                            JOIN items USING (itemnumber) 
3614                            JOIN biblio USING (biblionumber)
3615                            WHERE issues.itemnumber=?");
3616     $sth->execute($itemnumber);
3617     my $issues=$sth->fetchrow_hashref();
3618
3619     # If a borrower lost the item, add a replacement cost to the their record
3620     if ( my $borrowernumber = $issues->{borrowernumber} ){
3621         my $patron = Koha::Patrons->find( $borrowernumber );
3622
3623         if (C4::Context->preference('WhenLostForgiveFine')){
3624             my $fix = _FixOverduesOnReturn($borrowernumber, $itemnumber, 1, 0); # 1, 0 = exemptfine, no-dropbox
3625             defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $itemnumber...) failed!";  # zero is OK, check defined
3626         }
3627         if (C4::Context->preference('WhenLostChargeReplacementFee')){
3628             C4::Accounts::chargelostitem($borrowernumber, $itemnumber, $issues->{'replacementprice'}, "Lost Item $issues->{'title'} $issues->{'barcode'}");
3629             #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3630             #warn " $issues->{'borrowernumber'}  /  $itemnumber ";
3631         }
3632
3633         MarkIssueReturned($borrowernumber,$itemnumber,undef,undef,$patron->privacy) if $mark_returned;
3634     }
3635 }
3636
3637 sub GetOfflineOperations {
3638     my $dbh = C4::Context->dbh;
3639     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3640     $sth->execute(C4::Context->userenv->{'branch'});
3641     my $results = $sth->fetchall_arrayref({});
3642     return $results;
3643 }
3644
3645 sub GetOfflineOperation {
3646     my $operationid = shift;
3647     return unless $operationid;
3648     my $dbh = C4::Context->dbh;
3649     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3650     $sth->execute( $operationid );
3651     return $sth->fetchrow_hashref;
3652 }
3653
3654 sub AddOfflineOperation {
3655     my ( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount ) = @_;
3656     my $dbh = C4::Context->dbh;
3657     my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber, amount) VALUES(?,?,?,?,?,?,?)");
3658     $sth->execute( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount );
3659     return "Added.";
3660 }
3661
3662 sub DeleteOfflineOperation {
3663     my $dbh = C4::Context->dbh;
3664     my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3665     $sth->execute( shift );
3666     return "Deleted.";
3667 }
3668
3669 sub ProcessOfflineOperation {
3670     my $operation = shift;
3671
3672     my $report;
3673     if ( $operation->{action} eq 'return' ) {
3674         $report = ProcessOfflineReturn( $operation );
3675     } elsif ( $operation->{action} eq 'issue' ) {
3676         $report = ProcessOfflineIssue( $operation );
3677     } elsif ( $operation->{action} eq 'payment' ) {
3678         $report = ProcessOfflinePayment( $operation );
3679     }
3680
3681     DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3682
3683     return $report;
3684 }
3685
3686 sub ProcessOfflineReturn {
3687     my $operation = shift;
3688
3689     my $item = Koha::Items->find({barcode => $operation->{barcode}});
3690
3691     if ( $item ) {
3692         my $itemnumber = $item->itemnumber;
3693         my $issue = GetOpenIssue( $itemnumber );
3694         if ( $issue ) {
3695             MarkIssueReturned(
3696                 $issue->{borrowernumber},
3697                 $itemnumber,
3698                 undef,
3699                 $operation->{timestamp},
3700             );
3701             ModItem(
3702                 { renewals => 0, onloan => undef },
3703                 $issue->{'biblionumber'},
3704                 $itemnumber
3705             );
3706             return "Success.";
3707         } else {
3708             return "Item not issued.";
3709         }
3710     } else {
3711         return "Item not found.";
3712     }
3713 }
3714
3715 sub ProcessOfflineIssue {
3716     my $operation = shift;
3717
3718     my $patron = Koha::Patrons->find( { cardnumber => $operation->{cardnumber} } );
3719
3720     if ( $patron ) {
3721         my $item = Koha::Items->find({ barcode => $operation->{barcode} });
3722         unless ($item) {
3723             return "Barcode not found.";
3724         }
3725         my $itemnumber = $item->itemnumber;
3726         my $issue = GetOpenIssue( $itemnumber );
3727
3728         if ( $issue and ( $issue->{borrowernumber} ne $patron->borrowernumber ) ) { # Item already issued to another patron mark it returned
3729             MarkIssueReturned(
3730                 $issue->{borrowernumber},
3731                 $itemnumber,
3732                 undef,
3733                 $operation->{timestamp},
3734             );
3735         }
3736         AddIssue(
3737             $patron->unblessed,
3738             $operation->{'barcode'},
3739             undef,
3740             1,
3741             $operation->{timestamp},
3742             undef,
3743         );
3744         return "Success.";
3745     } else {
3746         return "Borrower not found.";
3747     }
3748 }
3749
3750 sub ProcessOfflinePayment {
3751     my $operation = shift;
3752
3753     my $patron = Koha::Patrons->find( { cardnumber => $operation->{cardnumber} });
3754     my $amount = $operation->{amount};
3755
3756     Koha::Account->new( { patron_id => $patron->id } )->pay( { amount => $amount } );
3757
3758     return "Success."
3759 }
3760
3761
3762 =head2 TransferSlip
3763
3764   TransferSlip($user_branch, $itemnumber, $barcode, $to_branch)
3765
3766   Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
3767
3768 =cut
3769
3770 sub TransferSlip {
3771     my ($branch, $itemnumber, $barcode, $to_branch) = @_;
3772
3773     my $item =  GetItem( $itemnumber, $barcode )
3774       or return;
3775
3776     return C4::Letters::GetPreparedLetter (
3777         module => 'circulation',
3778         letter_code => 'TRANSFERSLIP',
3779         branchcode => $branch,
3780         tables => {
3781             'branches'    => $to_branch,
3782             'biblio'      => $item->{biblionumber},
3783             'items'       => $item,
3784         },
3785     );
3786 }
3787
3788 =head2 CheckIfIssuedToPatron
3789
3790   CheckIfIssuedToPatron($borrowernumber, $biblionumber)
3791
3792   Return 1 if any record item is issued to patron, otherwise return 0
3793
3794 =cut
3795
3796 sub CheckIfIssuedToPatron {
3797     my ($borrowernumber, $biblionumber) = @_;
3798
3799     my $dbh = C4::Context->dbh;
3800     my $query = q|
3801         SELECT COUNT(*) FROM issues
3802         LEFT JOIN items ON items.itemnumber = issues.itemnumber
3803         WHERE items.biblionumber = ?
3804         AND issues.borrowernumber = ?
3805     |;
3806     my $is_issued = $dbh->selectrow_array($query, {}, $biblionumber, $borrowernumber );
3807     return 1 if $is_issued;
3808     return;
3809 }
3810
3811 =head2 IsItemIssued
3812
3813   IsItemIssued( $itemnumber )
3814
3815   Return 1 if the item is on loan, otherwise return 0
3816
3817 =cut
3818
3819 sub IsItemIssued {
3820     my $itemnumber = shift;
3821     my $dbh = C4::Context->dbh;
3822     my $sth = $dbh->prepare(q{
3823         SELECT COUNT(*)
3824         FROM issues
3825         WHERE itemnumber = ?
3826     });
3827     $sth->execute($itemnumber);
3828     return $sth->fetchrow;
3829 }
3830
3831 =head2 GetAgeRestriction
3832
3833   my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions, $borrower);
3834   my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions);
3835
3836   if($daysToAgeRestriction <= 0) { #Borrower is allowed to access this material, as they are older or as old as the agerestriction }
3837   if($daysToAgeRestriction > 0) { #Borrower is this many days from meeting the agerestriction }
3838
3839 @PARAM1 the koha.biblioitems.agerestriction value, like K18, PEGI 13, ...
3840 @PARAM2 a borrower-object with koha.borrowers.dateofbirth. (OPTIONAL)
3841 @RETURNS The age restriction age in years and the days to fulfill the age restriction for the given borrower.
3842          Negative days mean the borrower has gone past the age restriction age.
3843
3844 =cut
3845
3846 sub GetAgeRestriction {
3847     my ($record_restrictions, $borrower) = @_;
3848     my $markers = C4::Context->preference('AgeRestrictionMarker');
3849
3850     # Split $record_restrictions to something like FSK 16 or PEGI 6
3851     my @values = split ' ', uc($record_restrictions);
3852     return unless @values;
3853
3854     # Search first occurrence of one of the markers
3855     my @markers = split /\|/, uc($markers);
3856     return unless @markers;
3857
3858     my $index            = 0;
3859     my $restriction_year = 0;
3860     for my $value (@values) {
3861         $index++;
3862         for my $marker (@markers) {
3863             $marker =~ s/^\s+//;    #remove leading spaces
3864             $marker =~ s/\s+$//;    #remove trailing spaces
3865             if ( $marker eq $value ) {
3866                 if ( $index <= $#values ) {
3867                     $restriction_year += $values[$index];
3868                 }
3869                 last;
3870             }
3871             elsif ( $value =~ /^\Q$marker\E(\d+)$/ ) {
3872
3873                 # Perhaps it is something like "K16" (as in Finland)
3874                 $restriction_year += $1;
3875                 last;
3876             }
3877         }
3878         last if ( $restriction_year > 0 );
3879     }
3880
3881     #Check if the borrower is age restricted for this material and for how long.
3882     if ($restriction_year && $borrower) {
3883         if ( $borrower->{'dateofbirth'} ) {
3884             my @alloweddate = split /-/, $borrower->{'dateofbirth'};
3885             $alloweddate[0] += $restriction_year;
3886
3887             #Prevent runime eror on leap year (invalid date)
3888             if ( ( $alloweddate[1] == 2 ) && ( $alloweddate[2] == 29 ) ) {
3889                 $alloweddate[2] = 28;
3890             }
3891
3892             #Get how many days the borrower has to reach the age restriction
3893             my @Today = split /-/, DateTime->today->ymd();
3894             my $daysToAgeRestriction = Date_to_Days(@alloweddate) - Date_to_Days(@Today);
3895             #Negative days means the borrower went past the age restriction age
3896             return ($restriction_year, $daysToAgeRestriction);
3897         }
3898     }
3899
3900     return ($restriction_year);
3901 }
3902
3903
3904 =head2 GetPendingOnSiteCheckouts
3905
3906 =cut
3907
3908 sub GetPendingOnSiteCheckouts {
3909     my $dbh = C4::Context->dbh;
3910     return $dbh->selectall_arrayref(q|
3911         SELECT
3912           items.barcode,
3913           items.biblionumber,
3914           items.itemnumber,
3915           items.itemnotes,
3916           items.itemcallnumber,
3917           items.location,
3918           issues.date_due,
3919           issues.branchcode,
3920           issues.date_due < NOW() AS is_overdue,
3921           biblio.author,
3922           biblio.title,
3923           borrowers.firstname,
3924           borrowers.surname,
3925           borrowers.cardnumber,
3926           borrowers.borrowernumber
3927         FROM items
3928         LEFT JOIN issues ON items.itemnumber = issues.itemnumber
3929         LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
3930         LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
3931         WHERE issues.onsite_checkout = 1
3932     |, { Slice => {} } );
3933 }
3934
3935 sub GetTopIssues {
3936     my ($params) = @_;
3937
3938     my ($count, $branch, $itemtype, $ccode, $newness)
3939         = @$params{qw(count branch itemtype ccode newness)};
3940
3941     my $dbh = C4::Context->dbh;
3942     my $query = q{
3943         SELECT * FROM (
3944         SELECT b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
3945           bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
3946           i.ccode, SUM(i.issues) AS count
3947         FROM biblio b
3948         LEFT JOIN items i ON (i.biblionumber = b.biblionumber)
3949         LEFT JOIN biblioitems bi ON (bi.biblionumber = b.biblionumber)
3950     };
3951
3952     my (@where_strs, @where_args);
3953
3954     if ($branch) {
3955         push @where_strs, 'i.homebranch = ?';
3956         push @where_args, $branch;
3957     }
3958     if ($itemtype) {
3959         if (C4::Context->preference('item-level_itypes')){
3960             push @where_strs, 'i.itype = ?';
3961             push @where_args, $itemtype;
3962         } else {
3963             push @where_strs, 'bi.itemtype = ?';
3964             push @where_args, $itemtype;
3965         }
3966     }
3967     if ($ccode) {
3968         push @where_strs, 'i.ccode = ?';
3969         push @where_args, $ccode;
3970     }
3971     if ($newness) {
3972         push @where_strs, 'TO_DAYS(NOW()) - TO_DAYS(b.datecreated) <= ?';
3973         push @where_args, $newness;
3974     }
3975
3976     if (@where_strs) {
3977         $query .= 'WHERE ' . join(' AND ', @where_strs);
3978     }
3979
3980     $query .= q{
3981         GROUP BY b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode,
3982           bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size,
3983           i.ccode
3984         ORDER BY count DESC
3985     };
3986
3987     $query .= q{ ) xxx WHERE count > 0 };
3988     $count = int($count);
3989     if ($count > 0) {
3990         $query .= "LIMIT $count";
3991     }
3992
3993     my $rows = $dbh->selectall_arrayref($query, { Slice => {} }, @where_args);
3994
3995     return @$rows;
3996 }
3997
3998 sub _CalculateAndUpdateFine {
3999     my ($params) = @_;
4000
4001     my $borrower    = $params->{borrower};
4002     my $item        = $params->{item};
4003     my $issue       = $params->{issue};
4004     my $return_date = $params->{return_date};
4005
4006     unless ($borrower) { carp "No borrower passed in!" && return; }
4007     unless ($item)     { carp "No item passed in!"     && return; }
4008     unless ($issue)    { carp "No issue passed in!"    && return; }
4009
4010     my $datedue = dt_from_string( $issue->date_due );
4011
4012     # we only need to calculate and change the fines if we want to do that on return
4013     # Should be on for hourly loans
4014     my $control = C4::Context->preference('CircControl');
4015     my $control_branchcode =
4016         ( $control eq 'ItemHomeLibrary' ) ? $item->{homebranch}
4017       : ( $control eq 'PatronLibrary' )   ? $borrower->{branchcode}
4018       :                                     $issue->branchcode;
4019
4020     my $date_returned = $return_date ? dt_from_string($return_date) : dt_from_string();
4021
4022     my ( $amount, $type, $unitcounttotal ) =
4023       C4::Overdues::CalcFine( $item, $borrower->{categorycode}, $control_branchcode, $datedue, $date_returned );
4024
4025     $type ||= q{};
4026
4027     if ( C4::Context->preference('finesMode') eq 'production' ) {
4028         if ( $amount > 0 ) {
4029             C4::Overdues::UpdateFine({
4030                 issue_id       => $issue->issue_id,
4031                 itemnumber     => $issue->itemnumber,
4032                 borrowernumber => $issue->borrowernumber,
4033                 amount         => $amount,
4034                 type           => $type,
4035                 due            => output_pref($datedue),
4036             });
4037         }
4038         elsif ($return_date) {
4039
4040             # Backdated returns may have fines that shouldn't exist,
4041             # so in this case, we need to drop those fines to 0
4042
4043             C4::Overdues::UpdateFine({
4044                 issue_id       => $issue->issue_id,
4045                 itemnumber     => $issue->itemnumber,
4046                 borrowernumber => $issue->borrowernumber,
4047                 amount         => 0,
4048                 type           => $type,
4049                 due            => output_pref($datedue),
4050             });
4051         }
4052     }
4053 }
4054
4055 1;
4056
4057 __END__
4058
4059 =head1 AUTHOR
4060
4061 Koha Development Team <http://koha-community.org/>
4062
4063 =cut