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