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