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