Bug 14045: Change prototype of TooMany to raise a better warning
[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 C4::Context;
26 use C4::Stats;
27 use C4::Reserves;
28 use C4::Biblio;
29 use C4::Items;
30 use C4::Members;
31 use C4::Dates;
32 use C4::Dates qw(format_date);
33 use C4::Accounts;
34 use C4::ItemCirculationAlertPreference;
35 use C4::Message;
36 use C4::Debug;
37 use C4::Branch; # GetBranches
38 use C4::Log; # logaction
39 use C4::Koha qw(
40     GetAuthorisedValueByCode
41     GetAuthValCode
42     GetKohaAuthorisedValueLib
43 );
44 use C4::Overdues qw(CalcFine UpdateFine get_chargeable_units);
45 use C4::RotatingCollections qw(GetCollectionItemBranches);
46 use Algorithm::CheckDigits;
47
48 use Data::Dumper;
49 use Koha::DateUtils;
50 use Koha::Calendar;
51 use Koha::Borrower::Debarments;
52 use Koha::Database;
53 use Carp;
54 use List::MoreUtils qw( uniq );
55 use Date::Calc qw(
56   Today
57   Today_and_Now
58   Add_Delta_YM
59   Add_Delta_DHMS
60   Date_to_Days
61   Day_of_Week
62   Add_Delta_Days
63 );
64 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
65
66 BEGIN {
67         require Exporter;
68     $VERSION = 3.07.00.049;     # for version checking
69         @ISA    = qw(Exporter);
70
71         # FIXME subs that should probably be elsewhere
72         push @EXPORT, qw(
73                 &barcodedecode
74         &LostItem
75         &ReturnLostItem
76         &GetPendingOnSiteCheckouts
77         );
78
79         # subs to deal with issuing a book
80         push @EXPORT, qw(
81                 &CanBookBeIssued
82                 &CanBookBeRenewed
83                 &AddIssue
84                 &AddRenewal
85                 &GetRenewCount
86         &GetSoonestRenewDate
87                 &GetItemIssue
88                 &GetItemIssues
89                 &GetIssuingCharges
90                 &GetIssuingRule
91         &GetBranchBorrowerCircRule
92         &GetBranchItemRule
93                 &GetBiblioIssues
94                 &GetOpenIssue
95                 &AnonymiseIssueHistory
96         &CheckIfIssuedToPatron
97         &IsItemIssued
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, $duedatespec, $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<$duedatespec> is a C4::Dates 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'} = format_date($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'} = format_date($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 C4::Dates 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 C4::Dates 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 This will first check for a specific branch and
1573 category match from branch_borrower_circ_rules. 
1574
1575 If no rule is found, it will then check default_branch_circ_rules
1576 (same branch, default category).  If no rule is found,
1577 it will then check default_borrower_circ_rules (default 
1578 branch, same category), then failing that, default_circ_rules
1579 (default branch, default category).
1580
1581 If no rule has been found in the database, it will default to
1582 the buillt in rule:
1583
1584 maxissueqty - undef
1585
1586 C<$branchcode> and C<$categorycode> should contain the
1587 literal branch code and patron category code, respectively - no
1588 wildcards.
1589
1590 =cut
1591
1592 sub GetBranchBorrowerCircRule {
1593     my $branchcode = shift;
1594     my $categorycode = shift;
1595
1596     my $branch_cat_query = "SELECT maxissueqty
1597                             FROM branch_borrower_circ_rules
1598                             WHERE branchcode = ?
1599                             AND   categorycode = ?";
1600     my $dbh = C4::Context->dbh();
1601     my $sth = $dbh->prepare($branch_cat_query);
1602     $sth->execute($branchcode, $categorycode);
1603     my $result;
1604     if ($result = $sth->fetchrow_hashref()) {
1605         return $result;
1606     }
1607
1608     # try same branch, default borrower category
1609     my $branch_query = "SELECT maxissueqty
1610                         FROM default_branch_circ_rules
1611                         WHERE branchcode = ?";
1612     $sth = $dbh->prepare($branch_query);
1613     $sth->execute($branchcode);
1614     if ($result = $sth->fetchrow_hashref()) {
1615         return $result;
1616     }
1617
1618     # try default branch, same borrower category
1619     my $category_query = "SELECT maxissueqty
1620                           FROM default_borrower_circ_rules
1621                           WHERE categorycode = ?";
1622     $sth = $dbh->prepare($category_query);
1623     $sth->execute($categorycode);
1624     if ($result = $sth->fetchrow_hashref()) {
1625         return $result;
1626     }
1627   
1628     # try default branch, default borrower category
1629     my $default_query = "SELECT maxissueqty
1630                           FROM default_circ_rules";
1631     $sth = $dbh->prepare($default_query);
1632     $sth->execute();
1633     if ($result = $sth->fetchrow_hashref()) {
1634         return $result;
1635     }
1636     
1637     # built-in default circulation rule
1638     return {
1639         maxissueqty => undef,
1640     };
1641 }
1642
1643 =head2 GetBranchItemRule
1644
1645   my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
1646
1647 Retrieves circulation rule attributes that apply to the given
1648 branch and item type, regardless of patron category.
1649
1650 The return value is a hashref containing the following keys:
1651
1652 holdallowed => Hold policy for this branch and itemtype. Possible values:
1653   0: No holds allowed.
1654   1: Holds allowed only by patrons that have the same homebranch as the item.
1655   2: Holds allowed from any patron.
1656
1657 returnbranch => branch to which to return item.  Possible values:
1658   noreturn: do not return, let item remain where checked in (floating collections)
1659   homebranch: return to item's home branch
1660   holdingbranch: return to issuer branch
1661
1662 This searches branchitemrules in the following order:
1663
1664   * Same branchcode and itemtype
1665   * Same branchcode, itemtype '*'
1666   * branchcode '*', same itemtype
1667   * branchcode and itemtype '*'
1668
1669 Neither C<$branchcode> nor C<$itemtype> should be '*'.
1670
1671 =cut
1672
1673 sub GetBranchItemRule {
1674     my ( $branchcode, $itemtype ) = @_;
1675     my $dbh = C4::Context->dbh();
1676     my $result = {};
1677
1678     my @attempts = (
1679         ['SELECT holdallowed, returnbranch
1680             FROM branch_item_rules
1681             WHERE branchcode = ?
1682               AND itemtype = ?', $branchcode, $itemtype],
1683         ['SELECT holdallowed, returnbranch
1684             FROM default_branch_circ_rules
1685             WHERE branchcode = ?', $branchcode],
1686         ['SELECT holdallowed, returnbranch
1687             FROM default_branch_item_rules
1688             WHERE itemtype = ?', $itemtype],
1689         ['SELECT holdallowed, returnbranch
1690             FROM default_circ_rules'],
1691     );
1692
1693     foreach my $attempt (@attempts) {
1694         my ($query, @bind_params) = @{$attempt};
1695         my $search_result = $dbh->selectrow_hashref ( $query , {}, @bind_params )
1696           or next;
1697
1698         # Since branch/category and branch/itemtype use the same per-branch
1699         # defaults tables, we have to check that the key we want is set, not
1700         # just that a row was returned
1701         $result->{'holdallowed'}  = $search_result->{'holdallowed'}  unless ( defined $result->{'holdallowed'} );
1702         $result->{'returnbranch'} = $search_result->{'returnbranch'} unless ( defined $result->{'returnbranch'} );
1703     }
1704     
1705     # built-in default circulation rule
1706     $result->{'holdallowed'} = 2 unless ( defined $result->{'holdallowed'} );
1707     $result->{'returnbranch'} = 'homebranch' unless ( defined $result->{'returnbranch'} );
1708
1709     return $result;
1710 }
1711
1712 =head2 AddReturn
1713
1714   ($doreturn, $messages, $iteminformation, $borrower) =
1715       &AddReturn( $barcode, $branch [,$exemptfine] [,$dropbox] [,$returndate] );
1716
1717 Returns a book.
1718
1719 =over 4
1720
1721 =item C<$barcode> is the bar code of the book being returned.
1722
1723 =item C<$branch> is the code of the branch where the book is being returned.
1724
1725 =item C<$exemptfine> indicates that overdue charges for the item will be
1726 removed. Optional.
1727
1728 =item C<$dropbox> indicates that the check-in date is assumed to be
1729 yesterday, or the last non-holiday as defined in C4::Calendar .  If
1730 overdue charges are applied and C<$dropbox> is true, the last charge
1731 will be removed.  This assumes that the fines accrual script has run
1732 for _today_. Optional.
1733
1734 =item C<$return_date> allows the default return date to be overridden
1735 by the given return date. Optional.
1736
1737 =back
1738
1739 C<&AddReturn> returns a list of four items:
1740
1741 C<$doreturn> is true iff the return succeeded.
1742
1743 C<$messages> is a reference-to-hash giving feedback on the operation.
1744 The keys of the hash are:
1745
1746 =over 4
1747
1748 =item C<BadBarcode>
1749
1750 No item with this barcode exists. The value is C<$barcode>.
1751
1752 =item C<NotIssued>
1753
1754 The book is not currently on loan. The value is C<$barcode>.
1755
1756 =item C<IsPermanent>
1757
1758 The book's home branch is a permanent collection. If you have borrowed
1759 this book, you are not allowed to return it. The value is the code for
1760 the book's home branch.
1761
1762 =item C<withdrawn>
1763
1764 This book has been withdrawn/cancelled. The value should be ignored.
1765
1766 =item C<Wrongbranch>
1767
1768 This book has was returned to the wrong branch.  The value is a hashref
1769 so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
1770 contain the branchcode of the incorrect and correct return library, respectively.
1771
1772 =item C<ResFound>
1773
1774 The item was reserved. The value is a reference-to-hash whose keys are
1775 fields from the reserves table of the Koha database, and
1776 C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
1777 either C<Waiting>, C<Reserved>, or 0.
1778
1779 =item C<WasReturned>
1780
1781 Value 1 if return is successful.
1782
1783 =item C<NeedsTransfer>
1784
1785 If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer.
1786
1787 =back
1788
1789 C<$iteminformation> is a reference-to-hash, giving information about the
1790 returned item from the issues table.
1791
1792 C<$borrower> is a reference-to-hash, giving information about the
1793 patron who last borrowed the book.
1794
1795 =cut
1796
1797 sub AddReturn {
1798     my ( $barcode, $branch, $exemptfine, $dropbox, $return_date, $dropboxdate ) = @_;
1799
1800     if ($branch and not GetBranchDetail($branch)) {
1801         warn "AddReturn error: branch '$branch' not found.  Reverting to " . C4::Context->userenv->{'branch'};
1802         undef $branch;
1803     }
1804     $branch = C4::Context->userenv->{'branch'} unless $branch;  # we trust userenv to be a safe fallback/default
1805     my $messages;
1806     my $borrower;
1807     my $biblio;
1808     my $doreturn       = 1;
1809     my $validTransfert = 0;
1810     my $stat_type = 'return';
1811
1812     # get information on item
1813     my $itemnumber = GetItemnumberFromBarcode( $barcode );
1814     unless ($itemnumber) {
1815         return (0, { BadBarcode => $barcode }); # no barcode means no item or borrower.  bail out.
1816     }
1817     my $issue  = GetItemIssue($itemnumber);
1818     if ($issue and $issue->{borrowernumber}) {
1819         $borrower = C4::Members::GetMemberDetails($issue->{borrowernumber})
1820             or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existent borrowernumber '$issue->{borrowernumber}'\n"
1821                 . Dumper($issue) . "\n";
1822     } else {
1823         $messages->{'NotIssued'} = $barcode;
1824         # even though item is not on loan, it may still be transferred;  therefore, get current branch info
1825         $doreturn = 0;
1826         # No issue, no borrowernumber.  ONLY if $doreturn, *might* you have a $borrower later.
1827         # Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
1828         if (C4::Context->preference("RecordLocalUseOnReturn")) {
1829            $messages->{'LocalUse'} = 1;
1830            $stat_type = 'localuse';
1831         }
1832     }
1833
1834     my $item = GetItem($itemnumber) or die "GetItem($itemnumber) failed";
1835
1836     if ( $item->{'location'} eq 'PROC' ) {
1837         if ( C4::Context->preference("InProcessingToShelvingCart") ) {
1838             $item->{'location'} = 'CART';
1839         }
1840         else {
1841             $item->{location} = $item->{permanent_location};
1842         }
1843
1844         ModItem( $item, $item->{'biblionumber'}, $item->{'itemnumber'} );
1845     }
1846
1847         # full item data, but no borrowernumber or checkout info (no issue)
1848         # we know GetItem should work because GetItemnumberFromBarcode worked
1849     my $hbr = GetBranchItemRule($item->{'homebranch'}, $item->{'itype'})->{'returnbranch'} || "homebranch";
1850         # get the proper branch to which to return the item
1851     my $returnbranch = $item->{$hbr} || $branch ;
1852         # if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
1853
1854     my $borrowernumber = $borrower->{'borrowernumber'} || undef;    # we don't know if we had a borrower or not
1855
1856     my $yaml = C4::Context->preference('UpdateNotForLoanStatusOnCheckin');
1857     if ($yaml) {
1858         $yaml = "$yaml\n\n";  # YAML is anal on ending \n. Surplus does not hurt
1859         my $rules;
1860         eval { $rules = YAML::Load($yaml); };
1861         if ($@) {
1862             warn "Unable to parse UpdateNotForLoanStatusOnCheckin syspref : $@";
1863         }
1864         else {
1865             foreach my $key ( keys %$rules ) {
1866                 if ( $item->{notforloan} eq $key ) {
1867                     $messages->{'NotForLoanStatusUpdated'} = { from => $item->{notforloan}, to => $rules->{$key} };
1868                     ModItem( { notforloan => $rules->{$key} }, undef, $itemnumber );
1869                     last;
1870                 }
1871             }
1872         }
1873     }
1874
1875
1876     # check if the book is in a permanent collection....
1877     # FIXME -- This 'PE' attribute is largely undocumented.  afaict, there's no user interface that reflects this functionality.
1878     if ( $returnbranch ) {
1879         my $branches = GetBranches();    # a potentially expensive call for a non-feature.
1880         $branches->{$returnbranch}->{PE} and $messages->{'IsPermanent'} = $returnbranch;
1881     }
1882
1883     # check if the return is allowed at this branch
1884     my ($returnallowed, $message) = CanBookBeReturned($item, $branch);
1885     unless ($returnallowed){
1886         $messages->{'Wrongbranch'} = {
1887             Wrongbranch => $branch,
1888             Rightbranch => $message
1889         };
1890         $doreturn = 0;
1891         return ( $doreturn, $messages, $issue, $borrower );
1892     }
1893
1894     if ( $item->{'withdrawn'} ) { # book has been cancelled
1895         $messages->{'withdrawn'} = 1;
1896         $doreturn = 0 if C4::Context->preference("BlockReturnOfWithdrawnItems");
1897     }
1898
1899     # case of a return of document (deal with issues and holdingbranch)
1900     my $today = DateTime->now( time_zone => C4::Context->tz() );
1901
1902     if ($doreturn) {
1903         my $datedue = $issue->{date_due};
1904         $borrower or warn "AddReturn without current borrower";
1905                 my $circControlBranch;
1906         if ($dropbox) {
1907             # define circControlBranch only if dropbox mode is set
1908             # don't allow dropbox mode to create an invalid entry in issues (issuedate > today)
1909             # FIXME: check issuedate > returndate, factoring in holidays
1910             #$circControlBranch = _GetCircControlBranch($item,$borrower) unless ( $item->{'issuedate'} eq C4::Dates->today('iso') );;
1911             $circControlBranch = _GetCircControlBranch($item,$borrower);
1912             $issue->{'overdue'} = DateTime->compare($issue->{'date_due'}, $dropboxdate ) == -1 ? 1 : 0;
1913         }
1914
1915         if ($borrowernumber) {
1916             if ( ( C4::Context->preference('CalculateFinesOnReturn') && $issue->{'overdue'} ) || $return_date ) {
1917                 # we only need to calculate and change the fines if we want to do that on return
1918                 # Should be on for hourly loans
1919                 my $control = C4::Context->preference('CircControl');
1920                 my $control_branchcode =
1921                     ( $control eq 'ItemHomeLibrary' ) ? $item->{homebranch}
1922                   : ( $control eq 'PatronLibrary' )   ? $borrower->{branchcode}
1923                   :                                     $issue->{branchcode};
1924
1925                 my $date_returned =
1926                   $return_date ? dt_from_string($return_date) : $today;
1927
1928                 my ( $amount, $type, $unitcounttotal ) =
1929                   C4::Overdues::CalcFine( $item, $borrower->{categorycode},
1930                     $control_branchcode, $datedue, $date_returned );
1931
1932                 $type ||= q{};
1933
1934                 if ( C4::Context->preference('finesMode') eq 'production' ) {
1935                     if ( $amount > 0 ) {
1936                         C4::Overdues::UpdateFine( $issue->{itemnumber},
1937                             $issue->{borrowernumber},
1938                             $amount, $type, output_pref($datedue) );
1939                     }
1940                     elsif ($return_date) {
1941
1942                        # Backdated returns may have fines that shouldn't exist,
1943                        # so in this case, we need to drop those fines to 0
1944
1945                         C4::Overdues::UpdateFine( $issue->{itemnumber},
1946                             $issue->{borrowernumber},
1947                             0, $type, output_pref($datedue) );
1948                     }
1949                 }
1950             }
1951
1952             eval {
1953                 MarkIssueReturned( $borrowernumber, $item->{'itemnumber'},
1954                     $circControlBranch, $return_date, $borrower->{'privacy'} );
1955             };
1956             if ( $@ ) {
1957                 $messages->{'Wrongbranch'} = {
1958                     Wrongbranch => $branch,
1959                     Rightbranch => $message
1960                 };
1961                 carp $@;
1962                 return ( 0, { WasReturned => 0 }, $issue, $borrower );
1963             }
1964
1965             # FIXME is the "= 1" right?  This could be the borrower hash.
1966             $messages->{'WasReturned'} = 1;
1967
1968         }
1969
1970         ModItem({ onloan => undef }, $issue->{'biblionumber'}, $item->{'itemnumber'});
1971     }
1972
1973     # the holdingbranch is updated if the document is returned to another location.
1974     # this is always done regardless of whether the item was on loan or not
1975     if ($item->{'holdingbranch'} ne $branch) {
1976         UpdateHoldingbranch($branch, $item->{'itemnumber'});
1977         $item->{'holdingbranch'} = $branch; # update item data holdingbranch too
1978     }
1979     ModDateLastSeen( $item->{'itemnumber'} );
1980
1981     # check if we have a transfer for this document
1982     my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->{'itemnumber'} );
1983
1984     # if we have a transfer to do, we update the line of transfers with the datearrived
1985     my $is_in_rotating_collection = C4::RotatingCollections::isItemInAnyCollection( $item->{'itemnumber'} );
1986     if ($datesent) {
1987         if ( $tobranch eq $branch ) {
1988             my $sth = C4::Context->dbh->prepare(
1989                 "UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL"
1990             );
1991             $sth->execute( $item->{'itemnumber'} );
1992             # if we have a reservation with valid transfer, we can set it's status to 'W'
1993             ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
1994             C4::Reserves::ModReserveStatus($item->{'itemnumber'}, 'W');
1995         } else {
1996             $messages->{'WrongTransfer'}     = $tobranch;
1997             $messages->{'WrongTransferItem'} = $item->{'itemnumber'};
1998         }
1999         $validTransfert = 1;
2000     } else {
2001         ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
2002     }
2003
2004     # fix up the accounts.....
2005     if ( $item->{'itemlost'} ) {
2006         $messages->{'WasLost'} = 1;
2007
2008         if ( C4::Context->preference('RefundLostItemFeeOnReturn' ) ) {
2009             _FixAccountForLostAndReturned($item->{'itemnumber'}, $borrowernumber, $barcode);    # can tolerate undef $borrowernumber
2010             $messages->{'LostItemFeeRefunded'} = 1;
2011         }
2012     }
2013
2014     # fix up the overdues in accounts...
2015     if ($borrowernumber) {
2016         my $fix = _FixOverduesOnReturn($borrowernumber, $item->{itemnumber}, $exemptfine, $dropbox);
2017         defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $item->{itemnumber}...) failed!";  # zero is OK, check defined
2018         
2019         if ( $issue->{overdue} && $issue->{date_due} ) {
2020         # fix fine days
2021             $today = $dropboxdate if $dropbox;
2022             my ($debardate,$reminder) = _debar_user_on_return( $borrower, $item, $issue->{date_due}, $today );
2023             if ($reminder){
2024                 $messages->{'PrevDebarred'} = $debardate;
2025             } else {
2026                 $messages->{'Debarred'} = $debardate if $debardate;
2027             }
2028         # there's no overdue on the item but borrower had been previously debarred
2029         } elsif ( $issue->{date_due} and $borrower->{'debarred'} ) {
2030              if ( $borrower->{debarred} eq "9999-12-31") {
2031                 $messages->{'ForeverDebarred'} = $borrower->{'debarred'};
2032              } else {
2033                   my $borrower_debar_dt = dt_from_string( $borrower->{debarred} );
2034                   $borrower_debar_dt->truncate(to => 'day');
2035                   my $today_dt = $today->clone()->truncate(to => 'day');
2036                   if ( DateTime->compare( $borrower_debar_dt, $today_dt ) != -1 ) {
2037                       $messages->{'PrevDebarred'} = $borrower->{'debarred'};
2038                   }
2039              }
2040         }
2041     }
2042
2043     # find reserves.....
2044     # if we don't have a reserve with the status W, we launch the Checkreserves routine
2045     my ($resfound, $resrec);
2046     my $lookahead= C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
2047     ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->{'itemnumber'}, undef, $lookahead ) unless ( $item->{'withdrawn'} );
2048     if ($resfound) {
2049           $resrec->{'ResFound'} = $resfound;
2050         $messages->{'ResFound'} = $resrec;
2051     }
2052
2053     # Record the fact that this book was returned.
2054     # FIXME itemtype should record item level type, not bibliolevel type
2055     UpdateStats({
2056                 branch => $branch,
2057                 type => $stat_type,
2058                 itemnumber => $item->{'itemnumber'},
2059                 itemtype => $biblio->{'itemtype'},
2060                 borrowernumber => $borrowernumber,
2061                 ccode => $item->{'ccode'}}
2062     );
2063
2064     # Send a check-in slip. # NOTE: borrower may be undef.  probably shouldn't try to send messages then.
2065     my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2066     my %conditions = (
2067         branchcode   => $branch,
2068         categorycode => $borrower->{categorycode},
2069         item_type    => $item->{itype},
2070         notification => 'CHECKIN',
2071     );
2072     if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
2073         SendCirculationAlert({
2074             type     => 'CHECKIN',
2075             item     => $item,
2076             borrower => $borrower,
2077             branch   => $branch,
2078         });
2079     }
2080     
2081     logaction("CIRCULATION", "RETURN", $borrowernumber, $item->{'itemnumber'})
2082         if C4::Context->preference("ReturnLog");
2083     
2084     # Remove any OVERDUES related debarment if the borrower has no overdues
2085     if ( $borrowernumber
2086       && $borrower->{'debarred'}
2087       && C4::Context->preference('AutoRemoveOverduesRestrictions')
2088       && !C4::Members::HasOverdues( $borrowernumber )
2089       && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2090     ) {
2091         DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2092     }
2093
2094     # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer
2095     if (!$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) and not $messages->{'WrongTransfer'}){
2096         if  (C4::Context->preference("AutomaticItemReturn"    ) or
2097             (C4::Context->preference("UseBranchTransferLimits") and
2098              ! IsBranchTransferAllowed($branch, $returnbranch, $item->{C4::Context->preference("BranchTransferLimitsType")} )
2099            )) {
2100             $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s)", $item->{'itemnumber'},$branch, $returnbranch;
2101             $debug and warn "item: " . Dumper($item);
2102             ModItemTransfer($item->{'itemnumber'}, $branch, $returnbranch);
2103             $messages->{'WasTransfered'} = 1;
2104         } else {
2105             $messages->{'NeedsTransfer'} = $returnbranch;
2106         }
2107     }
2108
2109     return ( $doreturn, $messages, $issue, $borrower );
2110 }
2111
2112 =head2 MarkIssueReturned
2113
2114   MarkIssueReturned($borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy);
2115
2116 Unconditionally marks an issue as being returned by
2117 moving the C<issues> row to C<old_issues> and
2118 setting C<returndate> to the current date, or
2119 the last non-holiday date of the branccode specified in
2120 C<dropbox_branch> .  Assumes you've already checked that 
2121 it's safe to do this, i.e. last non-holiday > issuedate.
2122
2123 if C<$returndate> is specified (in iso format), it is used as the date
2124 of the return. It is ignored when a dropbox_branch is passed in.
2125
2126 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
2127 the old_issue is immediately anonymised
2128
2129 Ideally, this function would be internal to C<C4::Circulation>,
2130 not exported, but it is currently needed by one 
2131 routine in C<C4::Accounts>.
2132
2133 =cut
2134
2135 sub MarkIssueReturned {
2136     my ( $borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy ) = @_;
2137
2138     my $anonymouspatron;
2139     if ( $privacy == 2 ) {
2140         # The default of 0 will not work due to foreign key constraints
2141         # The anonymisation will fail if AnonymousPatron is not a valid entry
2142         # We need to check if the anonymous patron exist, Koha will fail loudly if it does not
2143         # Note that a warning should appear on the about page (System information tab).
2144         $anonymouspatron = C4::Context->preference('AnonymousPatron');
2145         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."
2146             unless C4::Members::GetMember( borrowernumber => $anonymouspatron );
2147     }
2148     my $dbh   = C4::Context->dbh;
2149     my $query = 'UPDATE issues SET returndate=';
2150     my @bind;
2151     if ($dropbox_branch) {
2152         my $calendar = Koha::Calendar->new( branchcode => $dropbox_branch );
2153         my $dropboxdate = $calendar->addDate( DateTime->now( time_zone => C4::Context->tz), -1 );
2154         $query .= ' ? ';
2155         push @bind, $dropboxdate->strftime('%Y-%m-%d %H:%M');
2156     } elsif ($returndate) {
2157         $query .= ' ? ';
2158         push @bind, $returndate;
2159     } else {
2160         $query .= ' now() ';
2161     }
2162     $query .= ' WHERE  borrowernumber = ?  AND itemnumber = ?';
2163     push @bind, $borrowernumber, $itemnumber;
2164     # FIXME transaction
2165     my $sth_upd  = $dbh->prepare($query);
2166     $sth_upd->execute(@bind);
2167     my $sth_copy = $dbh->prepare('INSERT INTO old_issues SELECT * FROM issues
2168                                   WHERE borrowernumber = ?
2169                                   AND itemnumber = ?');
2170     $sth_copy->execute($borrowernumber, $itemnumber);
2171     # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
2172     if ( $privacy == 2) {
2173         my $sth_ano = $dbh->prepare("UPDATE old_issues SET borrowernumber=?
2174                                   WHERE borrowernumber = ?
2175                                   AND itemnumber = ?");
2176        $sth_ano->execute($anonymouspatron, $borrowernumber, $itemnumber);
2177     }
2178     my $sth_del  = $dbh->prepare("DELETE FROM issues
2179                                   WHERE borrowernumber = ?
2180                                   AND itemnumber = ?");
2181     $sth_del->execute($borrowernumber, $itemnumber);
2182
2183     ModItem( { 'onloan' => undef }, undef, $itemnumber );
2184 }
2185
2186 =head2 _debar_user_on_return
2187
2188     _debar_user_on_return($borrower, $item, $datedue, today);
2189
2190 C<$borrower> borrower hashref
2191
2192 C<$item> item hashref
2193
2194 C<$datedue> date due DateTime object
2195
2196 C<$today> DateTime object representing the return time
2197
2198 Internal function, called only by AddReturn that calculates and updates
2199  the user fine days, and debars him if necessary.
2200
2201 Should only be called for overdue returns
2202
2203 =cut
2204
2205 sub _debar_user_on_return {
2206     my ( $borrower, $item, $dt_due, $dt_today ) = @_;
2207
2208     my $branchcode = _GetCircControlBranch( $item, $borrower );
2209
2210     my $circcontrol = C4::Context->preference('CircControl');
2211     my $issuingrule =
2212       GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode );
2213     my $finedays = $issuingrule->{finedays};
2214     my $unit     = $issuingrule->{lengthunit};
2215     my $chargeable_units = C4::Overdues::get_chargeable_units($unit, $dt_due, $dt_today, $branchcode);
2216
2217     if ($finedays) {
2218
2219         # finedays is in days, so hourly loans must multiply by 24
2220         # thus 1 hour late equals 1 day suspension * finedays rate
2221         $finedays = $finedays * 24 if ( $unit eq 'hours' );
2222
2223         # grace period is measured in the same units as the loan
2224         my $grace =
2225           DateTime::Duration->new( $unit => $issuingrule->{firstremind} );
2226
2227         my $deltadays = DateTime::Duration->new(
2228             days => $chargeable_units
2229         );
2230         if ( $deltadays->subtract($grace)->is_positive() ) {
2231             my $suspension_days = $deltadays * $finedays;
2232
2233             # If the max suspension days is < than the suspension days
2234             # the suspension days is limited to this maximum period.
2235             my $max_sd = $issuingrule->{maxsuspensiondays};
2236             if ( defined $max_sd ) {
2237                 $max_sd = DateTime::Duration->new( days => $max_sd );
2238                 $suspension_days = $max_sd
2239                   if DateTime::Duration->compare( $max_sd, $suspension_days ) < 0;
2240             }
2241
2242             my $new_debar_dt =
2243               $dt_today->clone()->add_duration( $suspension_days );
2244
2245             Koha::Borrower::Debarments::AddUniqueDebarment({
2246                 borrowernumber => $borrower->{borrowernumber},
2247                 expiration     => $new_debar_dt->ymd(),
2248                 type           => 'SUSPENSION',
2249             });
2250             # if borrower was already debarred but does not get an extra debarment
2251             if ( $borrower->{debarred} eq Koha::Borrower::Debarments::IsDebarred($borrower->{borrowernumber}) ) {
2252                     return ($borrower->{debarred},1);
2253             }
2254             return $new_debar_dt->ymd();
2255         }
2256     }
2257     return;
2258 }
2259
2260 =head2 _FixOverduesOnReturn
2261
2262    &_FixOverduesOnReturn($brn,$itm, $exemptfine, $dropboxmode);
2263
2264 C<$brn> borrowernumber
2265
2266 C<$itm> itemnumber
2267
2268 C<$exemptfine> BOOL -- remove overdue charge associated with this issue. 
2269 C<$dropboxmode> BOOL -- remove lastincrement on overdue charge associated with this issue.
2270
2271 Internal function, called only by AddReturn
2272
2273 =cut
2274
2275 sub _FixOverduesOnReturn {
2276     my ($borrowernumber, $item);
2277     unless ($borrowernumber = shift) {
2278         warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
2279         return;
2280     }
2281     unless ($item = shift) {
2282         warn "_FixOverduesOnReturn() not supplied valid itemnumber";
2283         return;
2284     }
2285     my ($exemptfine, $dropbox) = @_;
2286     my $dbh = C4::Context->dbh;
2287
2288     # check for overdue fine
2289     my $sth = $dbh->prepare(
2290 "SELECT * FROM accountlines WHERE (borrowernumber = ?) AND (itemnumber = ?) AND (accounttype='FU' OR accounttype='O')"
2291     );
2292     $sth->execute( $borrowernumber, $item );
2293
2294     # alter fine to show that the book has been returned
2295     my $data = $sth->fetchrow_hashref;
2296     return 0 unless $data;    # no warning, there's just nothing to fix
2297
2298     my $uquery;
2299     my @bind = ($data->{'accountlines_id'});
2300     if ($exemptfine) {
2301         $uquery = "update accountlines set accounttype='FFOR', amountoutstanding=0";
2302         if (C4::Context->preference("FinesLog")) {
2303             &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
2304         }
2305     } elsif ($dropbox && $data->{lastincrement}) {
2306         my $outstanding = $data->{amountoutstanding} - $data->{lastincrement} ;
2307         my $amt = $data->{amount} - $data->{lastincrement} ;
2308         if (C4::Context->preference("FinesLog")) {
2309             &logaction("FINES", 'MODIFY',$borrowernumber,"Dropbox adjustment $amt, item $item");
2310         }
2311          $uquery = "update accountlines set accounttype='F' ";
2312          if($outstanding  >= 0 && $amt >=0) {
2313             $uquery .= ", amount = ? , amountoutstanding=? ";
2314             unshift @bind, ($amt, $outstanding) ;
2315         }
2316     } else {
2317         $uquery = "update accountlines set accounttype='F' ";
2318     }
2319     $uquery .= " where (accountlines_id = ?)";
2320     my $usth = $dbh->prepare($uquery);
2321     return $usth->execute(@bind);
2322 }
2323
2324 =head2 _FixAccountForLostAndReturned
2325
2326   &_FixAccountForLostAndReturned($itemnumber, [$borrowernumber, $barcode]);
2327
2328 Calculates the charge for a book lost and returned.
2329
2330 Internal function, not exported, called only by AddReturn.
2331
2332 FIXME: This function reflects how inscrutable fines logic is.  Fix both.
2333 FIXME: Give a positive return value on success.  It might be the $borrowernumber who received credit, or the amount forgiven.
2334
2335 =cut
2336
2337 sub _FixAccountForLostAndReturned {
2338     my $itemnumber     = shift or return;
2339     my $borrowernumber = @_ ? shift : undef;
2340     my $item_id        = @_ ? shift : $itemnumber;  # Send the barcode if you want that logged in the description
2341     my $dbh = C4::Context->dbh;
2342     # check for charge made for lost book
2343     my $sth = $dbh->prepare("SELECT * FROM accountlines WHERE itemnumber = ? AND accounttype IN ('L', 'Rep', 'W') ORDER BY date DESC, accountno DESC");
2344     $sth->execute($itemnumber);
2345     my $data = $sth->fetchrow_hashref;
2346     $data or return;    # bail if there is nothing to do
2347     $data->{accounttype} eq 'W' and return;    # Written off
2348
2349     # writeoff this amount
2350     my $offset;
2351     my $amount = $data->{'amount'};
2352     my $acctno = $data->{'accountno'};
2353     my $amountleft;                                             # Starts off undef/zero.
2354     if ($data->{'amountoutstanding'} == $amount) {
2355         $offset     = $data->{'amount'};
2356         $amountleft = 0;                                        # Hey, it's zero here, too.
2357     } else {
2358         $offset     = $amount - $data->{'amountoutstanding'};   # Um, isn't this the same as ZERO?  We just tested those two things are ==
2359         $amountleft = $data->{'amountoutstanding'} - $amount;   # Um, isn't this the same as ZERO?  We just tested those two things are ==
2360     }
2361     my $usth = $dbh->prepare("UPDATE accountlines SET accounttype = 'LR',amountoutstanding='0'
2362         WHERE (accountlines_id = ?)");
2363     $usth->execute($data->{'accountlines_id'});      # We might be adjusting an account for some OTHER borrowernumber now.  Not the one we passed in.
2364     #check if any credit is left if so writeoff other accounts
2365     my $nextaccntno = getnextacctno($data->{'borrowernumber'});
2366     $amountleft *= -1 if ($amountleft < 0);
2367     if ($amountleft > 0) {
2368         my $msth = $dbh->prepare("SELECT * FROM accountlines WHERE (borrowernumber = ?)
2369                             AND (amountoutstanding >0) ORDER BY date");     # might want to order by amountoustanding ASC (pay smallest first)
2370         $msth->execute($data->{'borrowernumber'});
2371         # offset transactions
2372         my $newamtos;
2373         my $accdata;
2374         while (($accdata=$msth->fetchrow_hashref) and ($amountleft>0)){
2375             if ($accdata->{'amountoutstanding'} < $amountleft) {
2376                 $newamtos = 0;
2377                 $amountleft -= $accdata->{'amountoutstanding'};
2378             }  else {
2379                 $newamtos = $accdata->{'amountoutstanding'} - $amountleft;
2380                 $amountleft = 0;
2381             }
2382             my $thisacct = $accdata->{'accountlines_id'};
2383             # FIXME: move prepares outside while loop!
2384             my $usth = $dbh->prepare("UPDATE accountlines SET amountoutstanding= ?
2385                     WHERE (accountlines_id = ?)");
2386             $usth->execute($newamtos,$thisacct);
2387             $usth = $dbh->prepare("INSERT INTO accountoffsets
2388                 (borrowernumber, accountno, offsetaccount,  offsetamount)
2389                 VALUES
2390                 (?,?,?,?)");
2391             $usth->execute($data->{'borrowernumber'},$accdata->{'accountno'},$nextaccntno,$newamtos);
2392         }
2393     }
2394     $amountleft *= -1 if ($amountleft > 0);
2395     my $desc = "Item Returned " . $item_id;
2396     $usth = $dbh->prepare("INSERT INTO accountlines
2397         (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
2398         VALUES (?,?,now(),?,?,'CR',?)");
2399     $usth->execute($data->{'borrowernumber'},$nextaccntno,0-$amount,$desc,$amountleft);
2400     if ($borrowernumber) {
2401         # FIXME: same as query above.  use 1 sth for both
2402         $usth = $dbh->prepare("INSERT INTO accountoffsets
2403             (borrowernumber, accountno, offsetaccount,  offsetamount)
2404             VALUES (?,?,?,?)");
2405         $usth->execute($borrowernumber, $data->{'accountno'}, $nextaccntno, $offset);
2406     }
2407     ModItem({ paidfor => '' }, undef, $itemnumber);
2408     return;
2409 }
2410
2411 =head2 _GetCircControlBranch
2412
2413    my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
2414
2415 Internal function : 
2416
2417 Return the library code to be used to determine which circulation
2418 policy applies to a transaction.  Looks up the CircControl and
2419 HomeOrHoldingBranch system preferences.
2420
2421 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
2422
2423 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
2424
2425 =cut
2426
2427 sub _GetCircControlBranch {
2428     my ($item, $borrower) = @_;
2429     my $circcontrol = C4::Context->preference('CircControl');
2430     my $branch;
2431
2432     if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
2433         $branch= C4::Context->userenv->{'branch'};
2434     } elsif ($circcontrol eq 'PatronLibrary') {
2435         $branch=$borrower->{branchcode};
2436     } else {
2437         my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
2438         $branch = $item->{$branchfield};
2439         # default to item home branch if holdingbranch is used
2440         # and is not defined
2441         if (!defined($branch) && $branchfield eq 'holdingbranch') {
2442             $branch = $item->{homebranch};
2443         }
2444     }
2445     return $branch;
2446 }
2447
2448
2449
2450
2451
2452
2453 =head2 GetItemIssue
2454
2455   $issue = &GetItemIssue($itemnumber);
2456
2457 Returns patron currently having a book, or undef if not checked out.
2458
2459 C<$itemnumber> is the itemnumber.
2460
2461 C<$issue> is a hashref of the row from the issues table.
2462
2463 =cut
2464
2465 sub GetItemIssue {
2466     my ($itemnumber) = @_;
2467     return unless $itemnumber;
2468     my $sth = C4::Context->dbh->prepare(
2469         "SELECT items.*, issues.*
2470         FROM issues
2471         LEFT JOIN items ON issues.itemnumber=items.itemnumber
2472         WHERE issues.itemnumber=?");
2473     $sth->execute($itemnumber);
2474     my $data = $sth->fetchrow_hashref;
2475     return unless $data;
2476     $data->{issuedate_sql} = $data->{issuedate};
2477     $data->{date_due_sql} = $data->{date_due};
2478     $data->{issuedate} = dt_from_string($data->{issuedate}, 'sql');
2479     $data->{issuedate}->truncate(to => 'minute');
2480     $data->{date_due} = dt_from_string($data->{date_due}, 'sql');
2481     $data->{date_due}->truncate(to => 'minute');
2482     my $dt = DateTime->now( time_zone => C4::Context->tz)->truncate( to => 'minute');
2483     $data->{'overdue'} = DateTime->compare($data->{'date_due'}, $dt ) == -1 ? 1 : 0;
2484     return $data;
2485 }
2486
2487 =head2 GetOpenIssue
2488
2489   $issue = GetOpenIssue( $itemnumber );
2490
2491 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2492
2493 C<$itemnumber> is the item's itemnumber
2494
2495 Returns a hashref
2496
2497 =cut
2498
2499 sub GetOpenIssue {
2500   my ( $itemnumber ) = @_;
2501   return unless $itemnumber;
2502   my $dbh = C4::Context->dbh;  
2503   my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2504   $sth->execute( $itemnumber );
2505   return $sth->fetchrow_hashref();
2506
2507 }
2508
2509 =head2 GetIssues
2510
2511     $issues = GetIssues({});    # return all issues!
2512     $issues = GetIssues({ borrowernumber => $borrowernumber, biblionumber => $biblionumber });
2513
2514 Returns all pending issues that match given criteria.
2515 Returns a arrayref or undef if an error occurs.
2516
2517 Allowed criteria are:
2518
2519 =over 2
2520
2521 =item * borrowernumber
2522
2523 =item * biblionumber
2524
2525 =item * itemnumber
2526
2527 =back
2528
2529 =cut
2530
2531 sub GetIssues {
2532     my ($criteria) = @_;
2533
2534     # Build filters
2535     my @filters;
2536     my @allowed = qw(borrowernumber biblionumber itemnumber);
2537     foreach (@allowed) {
2538         if (defined $criteria->{$_}) {
2539             push @filters, {
2540                 field => $_,
2541                 value => $criteria->{$_},
2542             };
2543         }
2544     }
2545
2546     # Do we need to join other tables ?
2547     my %join;
2548     if (defined $criteria->{biblionumber}) {
2549         $join{items} = 1;
2550     }
2551
2552     # Build SQL query
2553     my $where = '';
2554     if (@filters) {
2555         $where = "WHERE " . join(' AND ', map { "$_->{field} = ?" } @filters);
2556     }
2557     my $query = q{
2558         SELECT issues.*
2559         FROM issues
2560     };
2561     if (defined $join{items}) {
2562         $query .= q{
2563             LEFT JOIN items ON (issues.itemnumber = items.itemnumber)
2564         };
2565     }
2566     $query .= $where;
2567
2568     # Execute SQL query
2569     my $dbh = C4::Context->dbh;
2570     my $sth = $dbh->prepare($query);
2571     my $rv = $sth->execute(map { $_->{value} } @filters);
2572
2573     return $rv ? $sth->fetchall_arrayref({}) : undef;
2574 }
2575
2576 =head2 GetItemIssues
2577
2578   $issues = &GetItemIssues($itemnumber, $history);
2579
2580 Returns patrons that have issued a book
2581
2582 C<$itemnumber> is the itemnumber
2583 C<$history> is false if you just want the current "issuer" (if any)
2584 and true if you want issues history from old_issues also.
2585
2586 Returns reference to an array of hashes
2587
2588 =cut
2589
2590 sub GetItemIssues {
2591     my ( $itemnumber, $history ) = @_;
2592     
2593     my $today = DateTime->now( time_zome => C4::Context->tz);  # get today date
2594     $today->truncate( to => 'minute' );
2595     my $sql = "SELECT * FROM issues
2596               JOIN borrowers USING (borrowernumber)
2597               JOIN items     USING (itemnumber)
2598               WHERE issues.itemnumber = ? ";
2599     if ($history) {
2600         $sql .= "UNION ALL
2601                  SELECT * FROM old_issues
2602                  LEFT JOIN borrowers USING (borrowernumber)
2603                  JOIN items USING (itemnumber)
2604                  WHERE old_issues.itemnumber = ? ";
2605     }
2606     $sql .= "ORDER BY date_due DESC";
2607     my $sth = C4::Context->dbh->prepare($sql);
2608     if ($history) {
2609         $sth->execute($itemnumber, $itemnumber);
2610     } else {
2611         $sth->execute($itemnumber);
2612     }
2613     my $results = $sth->fetchall_arrayref({});
2614     foreach (@$results) {
2615         my $date_due = dt_from_string($_->{date_due},'sql');
2616         $date_due->truncate( to => 'minute' );
2617
2618         $_->{overdue} = (DateTime->compare($date_due, $today) == -1) ? 1 : 0;
2619     }
2620     return $results;
2621 }
2622
2623 =head2 GetBiblioIssues
2624
2625   $issues = GetBiblioIssues($biblionumber);
2626
2627 this function get all issues from a biblionumber.
2628
2629 Return:
2630 C<$issues> is a reference to array which each value is ref-to-hash. This ref-to-hash containts all column from
2631 tables issues and the firstname,surname & cardnumber from borrowers.
2632
2633 =cut
2634
2635 sub GetBiblioIssues {
2636     my $biblionumber = shift;
2637     return unless $biblionumber;
2638     my $dbh   = C4::Context->dbh;
2639     my $query = "
2640         SELECT issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2641         FROM issues
2642             LEFT JOIN borrowers ON borrowers.borrowernumber = issues.borrowernumber
2643             LEFT JOIN items ON issues.itemnumber = items.itemnumber
2644             LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2645             LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2646         WHERE biblio.biblionumber = ?
2647         UNION ALL
2648         SELECT old_issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2649         FROM old_issues
2650             LEFT JOIN borrowers ON borrowers.borrowernumber = old_issues.borrowernumber
2651             LEFT JOIN items ON old_issues.itemnumber = items.itemnumber
2652             LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2653             LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2654         WHERE biblio.biblionumber = ?
2655         ORDER BY timestamp
2656     ";
2657     my $sth = $dbh->prepare($query);
2658     $sth->execute($biblionumber, $biblionumber);
2659
2660     my @issues;
2661     while ( my $data = $sth->fetchrow_hashref ) {
2662         push @issues, $data;
2663     }
2664     return \@issues;
2665 }
2666
2667 =head2 GetUpcomingDueIssues
2668
2669   my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2670
2671 =cut
2672
2673 sub GetUpcomingDueIssues {
2674     my $params = shift;
2675
2676     $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2677     my $dbh = C4::Context->dbh;
2678
2679     my $statement = <<END_SQL;
2680 SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2681 FROM issues 
2682 LEFT JOIN items USING (itemnumber)
2683 LEFT OUTER JOIN branches USING (branchcode)
2684 WHERE returndate is NULL
2685 HAVING days_until_due >= 0 AND days_until_due <= ?
2686 END_SQL
2687
2688     my @bind_parameters = ( $params->{'days_in_advance'} );
2689     
2690     my $sth = $dbh->prepare( $statement );
2691     $sth->execute( @bind_parameters );
2692     my $upcoming_dues = $sth->fetchall_arrayref({});
2693
2694     return $upcoming_dues;
2695 }
2696
2697 =head2 CanBookBeRenewed
2698
2699   ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber[, $override_limit]);
2700
2701 Find out whether a borrowed item may be renewed.
2702
2703 C<$borrowernumber> is the borrower number of the patron who currently
2704 has the item on loan.
2705
2706 C<$itemnumber> is the number of the item to renew.
2707
2708 C<$override_limit>, if supplied with a true value, causes
2709 the limit on the number of times that the loan can be renewed
2710 (as controlled by the item type) to be ignored. Overriding also allows
2711 to renew sooner than "No renewal before" and to manually renew loans
2712 that are automatically renewed.
2713
2714 C<$CanBookBeRenewed> returns a true value if the item may be renewed. The
2715 item must currently be on loan to the specified borrower; renewals
2716 must be allowed for the item's type; and the borrower must not have
2717 already renewed the loan. $error will contain the reason the renewal can not proceed
2718
2719 =cut
2720
2721 sub CanBookBeRenewed {
2722     my ( $borrowernumber, $itemnumber, $override_limit ) = @_;
2723
2724     my $dbh    = C4::Context->dbh;
2725     my $renews = 1;
2726
2727     my $item      = GetItem($itemnumber)      or return ( 0, 'no_item' );
2728     my $itemissue = GetItemIssue($itemnumber) or return ( 0, 'no_checkout' );
2729     return ( 0, 'onsite_checkout' ) if $itemissue->{onsite_checkout};
2730
2731     $borrowernumber ||= $itemissue->{borrowernumber};
2732     my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber )
2733       or return;
2734
2735     my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2736
2737     # This item can fill one or more unfilled reserve, can those unfilled reserves
2738     # all be filled by other available items?
2739     if ( $resfound
2740         && C4::Context->preference('AllowRenewalIfOtherItemsAvailable') )
2741     {
2742         my $schema = Koha::Database->new()->schema();
2743
2744         my $item_holds = $schema->resultset('Reserve')->search( { itemnumber => $itemnumber, found => undef } )->count();
2745         if ($item_holds) {
2746             # There is an item level hold on this item, no other item can fill the hold
2747             $resfound = 1;
2748         }
2749         else {
2750
2751             # Get all other items that could possibly fill reserves
2752             my @itemnumbers = $schema->resultset('Item')->search(
2753                 {
2754                     biblionumber => $resrec->{biblionumber},
2755                     onloan       => undef,
2756                     notforloan   => 0,
2757                     -not         => { itemnumber => $itemnumber }
2758                 },
2759                 { columns => 'itemnumber' }
2760             )->get_column('itemnumber')->all();
2761
2762             # Get all other reserves that could have been filled by this item
2763             my @borrowernumbers;
2764             while (1) {
2765                 my ( $reserve_found, $reserve, undef ) =
2766                   C4::Reserves::CheckReserves( $itemnumber, undef, undef, \@borrowernumbers );
2767
2768                 if ($reserve_found) {
2769                     push( @borrowernumbers, $reserve->{borrowernumber} );
2770                 }
2771                 else {
2772                     last;
2773                 }
2774             }
2775
2776             # If the count of the union of the lists of reservable items for each borrower
2777             # is equal or greater than the number of borrowers, we know that all reserves
2778             # can be filled with available items. We can get the union of the sets simply
2779             # by pushing all the elements onto an array and removing the duplicates.
2780             my @reservable;
2781             foreach my $b (@borrowernumbers) {
2782                 my ($borr) = C4::Members::GetMemberDetails($b);
2783                 foreach my $i (@itemnumbers) {
2784                     my $item = GetItem($i);
2785                     if (   IsAvailableForItemLevelRequest( $item, $borr )
2786                         && CanItemBeReserved( $b, $i )
2787                         && !IsItemOnHoldAndFound($i) )
2788                     {
2789                         push( @reservable, $i );
2790                     }
2791                 }
2792             }
2793
2794             @reservable = uniq(@reservable);
2795
2796             if ( @reservable >= @borrowernumbers ) {
2797                 $resfound = 0;
2798             }
2799         }
2800     }
2801
2802     return ( 0, "on_reserve" ) if $resfound;    # '' when no hold was found
2803
2804     return ( 1, undef ) if $override_limit;
2805
2806     my $branchcode = _GetCircControlBranch( $item, $borrower );
2807     my $issuingrule =
2808       GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode );
2809
2810     return ( 0, "too_many" )
2811       if $issuingrule->{renewalsallowed} <= $itemissue->{renewals};
2812
2813     if ( $issuingrule->{norenewalbefore} ) {
2814
2815         # Get current time and add norenewalbefore.
2816         # If this is smaller than date_due, it's too soon for renewal.
2817         if (
2818             DateTime->now( time_zone => C4::Context->tz() )->add(
2819                 $issuingrule->{lengthunit} => $issuingrule->{norenewalbefore}
2820             ) < $itemissue->{date_due}
2821           )
2822         {
2823             return ( 0, "auto_too_soon" ) if $itemissue->{auto_renew};
2824             return ( 0, "too_soon" );
2825         }
2826     }
2827
2828     return ( 0, "auto_renew" ) if $itemissue->{auto_renew};
2829     return ( 1, undef );
2830 }
2831
2832 =head2 AddRenewal
2833
2834   &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
2835
2836 Renews a loan.
2837
2838 C<$borrowernumber> is the borrower number of the patron who currently
2839 has the item.
2840
2841 C<$itemnumber> is the number of the item to renew.
2842
2843 C<$branch> is the library where the renewal took place (if any).
2844            The library that controls the circ policies for the renewal is retrieved from the issues record.
2845
2846 C<$datedue> can be a C4::Dates object used to set the due date.
2847
2848 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate.  If
2849 this parameter is not supplied, lastreneweddate is set to the current date.
2850
2851 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2852 from the book's item type.
2853
2854 =cut
2855
2856 sub AddRenewal {
2857     my $borrowernumber  = shift;
2858     my $itemnumber      = shift or return;
2859     my $branch          = shift;
2860     my $datedue         = shift;
2861     my $lastreneweddate = shift || DateTime->now(time_zone => C4::Context->tz)->ymd();
2862
2863     my $item   = GetItem($itemnumber) or return;
2864     my $biblio = GetBiblioFromItemNumber($itemnumber) or return;
2865
2866     my $dbh = C4::Context->dbh;
2867
2868     # Find the issues record for this book
2869     my $sth =
2870       $dbh->prepare("SELECT * FROM issues WHERE itemnumber = ?");
2871     $sth->execute( $itemnumber );
2872     my $issuedata = $sth->fetchrow_hashref;
2873
2874     return unless ( $issuedata );
2875
2876     $borrowernumber ||= $issuedata->{borrowernumber};
2877
2878     if ( defined $datedue && ref $datedue ne 'DateTime' ) {
2879         carp 'Invalid date passed to AddRenewal.';
2880         return;
2881     }
2882
2883     # If the due date wasn't specified, calculate it by adding the
2884     # book's loan length to today's date or the current due date
2885     # based on the value of the RenewalPeriodBase syspref.
2886     unless ($datedue) {
2887
2888         my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber ) or return;
2889         my $itemtype = (C4::Context->preference('item-level_itypes')) ? $biblio->{'itype'} : $biblio->{'itemtype'};
2890
2891         $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
2892                                         dt_from_string( $issuedata->{date_due} ) :
2893                                         DateTime->now( time_zone => C4::Context->tz());
2894         $datedue =  CalcDateDue($datedue, $itemtype, $issuedata->{'branchcode'}, $borrower, 'is a renewal');
2895     }
2896
2897     # Update the issues record to have the new due date, and a new count
2898     # of how many times it has been renewed.
2899     my $renews = $issuedata->{'renewals'} + 1;
2900     $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
2901                             WHERE borrowernumber=? 
2902                             AND itemnumber=?"
2903     );
2904
2905     $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
2906
2907     # Update the renewal count on the item, and tell zebra to reindex
2908     $renews = $biblio->{'renewals'} + 1;
2909     ModItem({ renewals => $renews, onloan => $datedue->strftime('%Y-%m-%d %H:%M')}, $biblio->{'biblionumber'}, $itemnumber);
2910
2911     # Charge a new rental fee, if applicable?
2912     my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
2913     if ( $charge > 0 ) {
2914         my $accountno = getnextacctno( $borrowernumber );
2915         my $item = GetBiblioFromItemNumber($itemnumber);
2916         my $manager_id = 0;
2917         $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv; 
2918         $sth = $dbh->prepare(
2919                 "INSERT INTO accountlines
2920                     (date, borrowernumber, accountno, amount, manager_id,
2921                     description,accounttype, amountoutstanding, itemnumber)
2922                     VALUES (now(),?,?,?,?,?,?,?,?)"
2923         );
2924         $sth->execute( $borrowernumber, $accountno, $charge, $manager_id,
2925             "Renewal of Rental Item $item->{'title'} $item->{'barcode'}",
2926             'Rent', $charge, $itemnumber );
2927     }
2928
2929     # Send a renewal slip according to checkout alert preferencei
2930     if ( C4::Context->preference('RenewalSendNotice') eq '1') {
2931         my $borrower = C4::Members::GetMemberDetails( $borrowernumber, 0 );
2932         my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2933         my %conditions = (
2934                 branchcode   => $branch,
2935                 categorycode => $borrower->{categorycode},
2936                 item_type    => $item->{itype},
2937                 notification => 'CHECKOUT',
2938         );
2939         if ($circulation_alert->is_enabled_for(\%conditions)) {
2940                 SendCirculationAlert({
2941                         type     => 'RENEWAL',
2942                         item     => $item,
2943                 borrower => $borrower,
2944                 branch   => $branch,
2945                 });
2946         }
2947     }
2948
2949     # Remove any OVERDUES related debarment if the borrower has no overdues
2950     my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber );
2951     if ( $borrowernumber
2952       && $borrower->{'debarred'}
2953       && !C4::Members::HasOverdues( $borrowernumber )
2954       && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) }
2955     ) {
2956         DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' });
2957     }
2958
2959     # Log the renewal
2960     UpdateStats({branch => $branch,
2961                 type => 'renew',
2962                 amount => $charge,
2963                 itemnumber => $itemnumber,
2964                 itemtype => $item->{itype},
2965                 borrowernumber => $borrowernumber,
2966                 ccode => $item->{'ccode'}}
2967                 );
2968         return $datedue;
2969 }
2970
2971 sub GetRenewCount {
2972     # check renewal status
2973     my ( $bornum, $itemno ) = @_;
2974     my $dbh           = C4::Context->dbh;
2975     my $renewcount    = 0;
2976     my $renewsallowed = 0;
2977     my $renewsleft    = 0;
2978
2979     my $borrower = C4::Members::GetMember( borrowernumber => $bornum);
2980     my $item     = GetItem($itemno); 
2981
2982     # Look in the issues table for this item, lent to this borrower,
2983     # and not yet returned.
2984
2985     # FIXME - I think this function could be redone to use only one SQL call.
2986     my $sth = $dbh->prepare(
2987         "select * from issues
2988                                 where (borrowernumber = ?)
2989                                 and (itemnumber = ?)"
2990     );
2991     $sth->execute( $bornum, $itemno );
2992     my $data = $sth->fetchrow_hashref;
2993     $renewcount = $data->{'renewals'} if $data->{'renewals'};
2994     # $item and $borrower should be calculated
2995     my $branchcode = _GetCircControlBranch($item, $borrower);
2996     
2997     my $issuingrule = GetIssuingRule($borrower->{categorycode}, $item->{itype}, $branchcode);
2998     
2999     $renewsallowed = $issuingrule->{'renewalsallowed'};
3000     $renewsleft    = $renewsallowed - $renewcount;
3001     if($renewsleft < 0){ $renewsleft = 0; }
3002     return ( $renewcount, $renewsallowed, $renewsleft );
3003 }
3004
3005 =head2 GetSoonestRenewDate
3006
3007   $NoRenewalBeforeThisDate = &GetSoonestRenewDate($borrowernumber, $itemnumber);
3008
3009 Find out the soonest possible renew date of a borrowed item.
3010
3011 C<$borrowernumber> is the borrower number of the patron who currently
3012 has the item on loan.
3013
3014 C<$itemnumber> is the number of the item to renew.
3015
3016 C<$GetSoonestRenewDate> returns the DateTime of the soonest possible
3017 renew date, based on the value "No renewal before" of the applicable
3018 issuing rule. Returns the current date if the item can already be
3019 renewed, and returns undefined if the borrower, loan, or item
3020 cannot be found.
3021
3022 =cut
3023
3024 sub GetSoonestRenewDate {
3025     my ( $borrowernumber, $itemnumber ) = @_;
3026
3027     my $dbh = C4::Context->dbh;
3028
3029     my $item      = GetItem($itemnumber)      or return;
3030     my $itemissue = GetItemIssue($itemnumber) or return;
3031
3032     $borrowernumber ||= $itemissue->{borrowernumber};
3033     my $borrower = C4::Members::GetMemberDetails($borrowernumber)
3034       or return;
3035
3036     my $branchcode = _GetCircControlBranch( $item, $borrower );
3037     my $issuingrule =
3038       GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode );
3039
3040     my $now = DateTime->now( time_zone => C4::Context->tz() );
3041
3042     if ( $issuingrule->{norenewalbefore} ) {
3043         my $soonestrenewal =
3044           $itemissue->{date_due}->subtract(
3045             $issuingrule->{lengthunit} => $issuingrule->{norenewalbefore} );
3046
3047         $soonestrenewal = $now > $soonestrenewal ? $now : $soonestrenewal;
3048         return $soonestrenewal;
3049     }
3050     return $now;
3051 }
3052
3053 =head2 GetIssuingCharges
3054
3055   ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
3056
3057 Calculate how much it would cost for a given patron to borrow a given
3058 item, including any applicable discounts.
3059
3060 C<$itemnumber> is the item number of item the patron wishes to borrow.
3061
3062 C<$borrowernumber> is the patron's borrower number.
3063
3064 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
3065 and C<$item_type> is the code for the item's item type (e.g., C<VID>
3066 if it's a video).
3067
3068 =cut
3069
3070 sub GetIssuingCharges {
3071
3072     # calculate charges due
3073     my ( $itemnumber, $borrowernumber ) = @_;
3074     my $charge = 0;
3075     my $dbh    = C4::Context->dbh;
3076     my $item_type;
3077
3078     # Get the book's item type and rental charge (via its biblioitem).
3079     my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
3080         LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
3081     $charge_query .= (C4::Context->preference('item-level_itypes'))
3082         ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
3083         : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
3084
3085     $charge_query .= ' WHERE items.itemnumber =?';
3086
3087     my $sth = $dbh->prepare($charge_query);
3088     $sth->execute($itemnumber);
3089     if ( my $item_data = $sth->fetchrow_hashref ) {
3090         $item_type = $item_data->{itemtype};
3091         $charge    = $item_data->{rentalcharge};
3092         my $branch = C4::Branch::mybranch();
3093         my $discount_query = q|SELECT rentaldiscount,
3094             issuingrules.itemtype, issuingrules.branchcode
3095             FROM borrowers
3096             LEFT JOIN issuingrules ON borrowers.categorycode = issuingrules.categorycode
3097             WHERE borrowers.borrowernumber = ?
3098             AND (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
3099             AND (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')|;
3100         my $discount_sth = $dbh->prepare($discount_query);
3101         $discount_sth->execute( $borrowernumber, $item_type, $branch );
3102         my $discount_rules = $discount_sth->fetchall_arrayref({});
3103         if (@{$discount_rules}) {
3104             # We may have multiple rules so get the most specific
3105             my $discount = _get_discount_from_rule($discount_rules, $branch, $item_type);
3106             $charge = ( $charge * ( 100 - $discount ) ) / 100;
3107         }
3108     }
3109
3110     return ( $charge, $item_type );
3111 }
3112
3113 # Select most appropriate discount rule from those returned
3114 sub _get_discount_from_rule {
3115     my ($rules_ref, $branch, $itemtype) = @_;
3116     my $discount;
3117
3118     if (@{$rules_ref} == 1) { # only 1 applicable rule use it
3119         $discount = $rules_ref->[0]->{rentaldiscount};
3120         return (defined $discount) ? $discount : 0;
3121     }
3122     # could have up to 4 does one match $branch and $itemtype
3123     my @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq $itemtype } @{$rules_ref};
3124     if (@d) {
3125         $discount = $d[0]->{rentaldiscount};
3126         return (defined $discount) ? $discount : 0;
3127     }
3128     # do we have item type + all branches
3129     @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq $itemtype } @{$rules_ref};
3130     if (@d) {
3131         $discount = $d[0]->{rentaldiscount};
3132         return (defined $discount) ? $discount : 0;
3133     }
3134     # do we all item types + this branch
3135     @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq q{*} } @{$rules_ref};
3136     if (@d) {
3137         $discount = $d[0]->{rentaldiscount};
3138         return (defined $discount) ? $discount : 0;
3139     }
3140     # so all and all (surely we wont get here)
3141     @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq q{*} } @{$rules_ref};
3142     if (@d) {
3143         $discount = $d[0]->{rentaldiscount};
3144         return (defined $discount) ? $discount : 0;
3145     }
3146     # none of the above
3147     return 0;
3148 }
3149
3150 =head2 AddIssuingCharge
3151
3152   &AddIssuingCharge( $itemno, $borrowernumber, $charge )
3153
3154 =cut
3155
3156 sub AddIssuingCharge {
3157     my ( $itemnumber, $borrowernumber, $charge ) = @_;
3158     my $dbh = C4::Context->dbh;
3159     my $nextaccntno = getnextacctno( $borrowernumber );
3160     my $manager_id = 0;
3161     $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
3162     my $query ="
3163         INSERT INTO accountlines
3164             (borrowernumber, itemnumber, accountno,
3165             date, amount, description, accounttype,
3166             amountoutstanding, manager_id)
3167         VALUES (?, ?, ?,now(), ?, 'Rental', 'Rent',?,?)
3168     ";
3169     my $sth = $dbh->prepare($query);
3170     $sth->execute( $borrowernumber, $itemnumber, $nextaccntno, $charge, $charge, $manager_id );
3171 }
3172
3173 =head2 GetTransfers
3174
3175   GetTransfers($itemnumber);
3176
3177 =cut
3178
3179 sub GetTransfers {
3180     my ($itemnumber) = @_;
3181
3182     my $dbh = C4::Context->dbh;
3183
3184     my $query = '
3185         SELECT datesent,
3186                frombranch,
3187                tobranch
3188         FROM branchtransfers
3189         WHERE itemnumber = ?
3190           AND datearrived IS NULL
3191         ';
3192     my $sth = $dbh->prepare($query);
3193     $sth->execute($itemnumber);
3194     my @row = $sth->fetchrow_array();
3195     return @row;
3196 }
3197
3198 =head2 GetTransfersFromTo
3199
3200   @results = GetTransfersFromTo($frombranch,$tobranch);
3201
3202 Returns the list of pending transfers between $from and $to branch
3203
3204 =cut
3205
3206 sub GetTransfersFromTo {
3207     my ( $frombranch, $tobranch ) = @_;
3208     return unless ( $frombranch && $tobranch );
3209     my $dbh   = C4::Context->dbh;
3210     my $query = "
3211         SELECT itemnumber,datesent,frombranch
3212         FROM   branchtransfers
3213         WHERE  frombranch=?
3214           AND  tobranch=?
3215           AND datearrived IS NULL
3216     ";
3217     my $sth = $dbh->prepare($query);
3218     $sth->execute( $frombranch, $tobranch );
3219     my @gettransfers;
3220
3221     while ( my $data = $sth->fetchrow_hashref ) {
3222         push @gettransfers, $data;
3223     }
3224     return (@gettransfers);
3225 }
3226
3227 =head2 DeleteTransfer
3228
3229   &DeleteTransfer($itemnumber);
3230
3231 =cut
3232
3233 sub DeleteTransfer {
3234     my ($itemnumber) = @_;
3235     return unless $itemnumber;
3236     my $dbh          = C4::Context->dbh;
3237     my $sth          = $dbh->prepare(
3238         "DELETE FROM branchtransfers
3239          WHERE itemnumber=?
3240          AND datearrived IS NULL "
3241     );
3242     return $sth->execute($itemnumber);
3243 }
3244
3245 =head2 AnonymiseIssueHistory
3246
3247   ($rows,$err_history_not_deleted) = AnonymiseIssueHistory($date,$borrowernumber)
3248
3249 This function write NULL instead of C<$borrowernumber> given on input arg into the table issues.
3250 if C<$borrowernumber> is not set, it will delete the issue history for all borrower older than C<$date>.
3251
3252 If c<$borrowernumber> is set, it will delete issue history for only that borrower, regardless of their opac privacy
3253 setting (force delete).
3254
3255 return the number of affected rows and a value that evaluates to true if an error occurred deleting the history.
3256
3257 =cut
3258
3259 sub AnonymiseIssueHistory {
3260     my $date           = shift;
3261     my $borrowernumber = shift;
3262     my $dbh            = C4::Context->dbh;
3263     my $query          = "
3264         UPDATE old_issues
3265         SET    borrowernumber = ?
3266         WHERE  returndate < ?
3267           AND borrowernumber IS NOT NULL
3268     ";
3269
3270     # The default of 0 does not work due to foreign key constraints
3271     # The anonymisation should not fail quietly if AnonymousPatron is not a valid entry
3272     # Set it to undef (NULL)
3273     my $anonymouspatron = C4::Context->preference('AnonymousPatron') || undef;
3274     my @bind_params = ($anonymouspatron, $date);
3275     if (defined $borrowernumber) {
3276        $query .= " AND borrowernumber = ?";
3277        push @bind_params, $borrowernumber;
3278     } else {
3279        $query .= " AND (SELECT privacy FROM borrowers WHERE borrowers.borrowernumber=old_issues.borrowernumber) <> 0";
3280     }
3281     my $sth = $dbh->prepare($query);
3282     $sth->execute(@bind_params);
3283     my $anonymisation_err = $dbh->err;
3284     my $rows_affected = $sth->rows;  ### doublecheck row count return function
3285     return ($rows_affected, $anonymisation_err);
3286 }
3287
3288 =head2 SendCirculationAlert
3289
3290 Send out a C<check-in> or C<checkout> alert using the messaging system.
3291
3292 B<Parameters>:
3293
3294 =over 4
3295
3296 =item type
3297
3298 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
3299
3300 =item item
3301
3302 Hashref of information about the item being checked in or out.
3303
3304 =item borrower
3305
3306 Hashref of information about the borrower of the item.
3307
3308 =item branch
3309
3310 The branchcode from where the checkout or check-in took place.
3311
3312 =back
3313
3314 B<Example>:
3315
3316     SendCirculationAlert({
3317         type     => 'CHECKOUT',
3318         item     => $item,
3319         borrower => $borrower,
3320         branch   => $branch,
3321     });
3322
3323 =cut
3324
3325 sub SendCirculationAlert {
3326     my ($opts) = @_;
3327     my ($type, $item, $borrower, $branch) =
3328         ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
3329     my %message_name = (
3330         CHECKIN  => 'Item_Check_in',
3331         CHECKOUT => 'Item_Checkout',
3332         RENEWAL  => 'Item_Checkout',
3333     );
3334     my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
3335         borrowernumber => $borrower->{borrowernumber},
3336         message_name   => $message_name{$type},
3337     });
3338     my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues';
3339
3340     my @transports = keys %{ $borrower_preferences->{transports} };
3341     # warn "no transports" unless @transports;
3342     for (@transports) {
3343         # warn "transport: $_";
3344         my $message = C4::Message->find_last_message($borrower, $type, $_);
3345         if (!$message) {
3346             #warn "create new message";
3347             my $letter =  C4::Letters::GetPreparedLetter (
3348                 module => 'circulation',
3349                 letter_code => $type,
3350                 branchcode => $branch,
3351                 message_transport_type => $_,
3352                 tables => {
3353                     $issues_table => $item->{itemnumber},
3354                     'items'       => $item->{itemnumber},
3355                     'biblio'      => $item->{biblionumber},
3356                     'biblioitems' => $item->{biblionumber},
3357                     'borrowers'   => $borrower,
3358                     'branches'    => $branch,
3359                 }
3360             ) or next;
3361             C4::Message->enqueue($letter, $borrower, $_);
3362         } else {
3363             #warn "append to old message";
3364             my $letter =  C4::Letters::GetPreparedLetter (
3365                 module => 'circulation',
3366                 letter_code => $type,
3367                 branchcode => $branch,
3368                 message_transport_type => $_,
3369                 tables => {
3370                     $issues_table => $item->{itemnumber},
3371                     'items'       => $item->{itemnumber},
3372                     'biblio'      => $item->{biblionumber},
3373                     'biblioitems' => $item->{biblionumber},
3374                     'borrowers'   => $borrower,
3375                     'branches'    => $branch,
3376                 }
3377             ) or next;
3378             $message->append($letter);
3379             $message->update;
3380         }
3381     }
3382
3383     return;
3384 }
3385
3386 =head2 updateWrongTransfer
3387
3388   $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
3389
3390 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 
3391
3392 =cut
3393
3394 sub updateWrongTransfer {
3395         my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
3396         my $dbh = C4::Context->dbh;     
3397 # first step validate the actual line of transfert .
3398         my $sth =
3399                 $dbh->prepare(
3400                         "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
3401                 );
3402                 $sth->execute($FromLibrary,$itemNumber);
3403
3404 # second step create a new line of branchtransfer to the right location .
3405         ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
3406
3407 #third step changing holdingbranch of item
3408         UpdateHoldingbranch($FromLibrary,$itemNumber);
3409 }
3410
3411 =head2 UpdateHoldingbranch
3412
3413   $items = UpdateHoldingbranch($branch,$itmenumber);
3414
3415 Simple methode for updating hodlingbranch in items BDD line
3416
3417 =cut
3418
3419 sub UpdateHoldingbranch {
3420         my ( $branch,$itemnumber ) = @_;
3421     ModItem({ holdingbranch => $branch }, undef, $itemnumber);
3422 }
3423
3424 =head2 CalcDateDue
3425
3426 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
3427
3428 this function calculates the due date given the start date and configured circulation rules,
3429 checking against the holidays calendar as per the 'useDaysMode' syspref.
3430 C<$startdate>   = C4::Dates object representing start date of loan period (assumed to be today)
3431 C<$itemtype>  = itemtype code of item in question
3432 C<$branch>  = location whose calendar to use
3433 C<$borrower> = Borrower object
3434 C<$isrenewal> = Boolean: is true if we want to calculate the date due for a renewal. Else is false.
3435
3436 =cut
3437
3438 sub CalcDateDue {
3439     my ( $startdate, $itemtype, $branch, $borrower, $isrenewal ) = @_;
3440
3441     $isrenewal ||= 0;
3442
3443     # loanlength now a href
3444     my $loanlength =
3445             GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
3446
3447     my $length_key = ( $isrenewal and defined $loanlength->{renewalperiod} )
3448             ? qq{renewalperiod}
3449             : qq{issuelength};
3450
3451     my $datedue;
3452     if ( $startdate ) {
3453         if (ref $startdate ne 'DateTime' ) {
3454             $datedue = dt_from_string($datedue);
3455         } else {
3456             $datedue = $startdate->clone;
3457         }
3458     } else {
3459         $datedue =
3460           DateTime->now( time_zone => C4::Context->tz() )
3461           ->truncate( to => 'minute' );
3462     }
3463
3464
3465     # calculate the datedue as normal
3466     if ( C4::Context->preference('useDaysMode') eq 'Days' )
3467     {    # ignoring calendar
3468         if ( $loanlength->{lengthunit} eq 'hours' ) {
3469             $datedue->add( hours => $loanlength->{$length_key} );
3470         } else {    # days
3471             $datedue->add( days => $loanlength->{$length_key} );
3472             $datedue->set_hour(23);
3473             $datedue->set_minute(59);
3474         }
3475     } else {
3476         my $dur;
3477         if ($loanlength->{lengthunit} eq 'hours') {
3478             $dur = DateTime::Duration->new( hours => $loanlength->{$length_key});
3479         }
3480         else { # days
3481             $dur = DateTime::Duration->new( days => $loanlength->{$length_key});
3482         }
3483         my $calendar = Koha::Calendar->new( branchcode => $branch );
3484         $datedue = $calendar->addDate( $datedue, $dur, $loanlength->{lengthunit} );
3485         if ($loanlength->{lengthunit} eq 'days') {
3486             $datedue->set_hour(23);
3487             $datedue->set_minute(59);
3488         }
3489     }
3490
3491     # if Hard Due Dates are used, retrieve them and apply as necessary
3492     my ( $hardduedate, $hardduedatecompare ) =
3493       GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
3494     if ($hardduedate) {    # hardduedates are currently dates
3495         $hardduedate->truncate( to => 'minute' );
3496         $hardduedate->set_hour(23);
3497         $hardduedate->set_minute(59);
3498         my $cmp = DateTime->compare( $hardduedate, $datedue );
3499
3500 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
3501 # if the calculated date is before the 'after' Hard Due Date (floor), override
3502 # if the hard due date is set to 'exactly', overrride
3503         if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
3504             $datedue = $hardduedate->clone;
3505         }
3506
3507         # in all other cases, keep the date due as it is
3508
3509     }
3510
3511     # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3512     if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3513         my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso', 'floating');
3514         if( $expiry_dt ) { #skip empty expiry date..
3515             $expiry_dt->set( hour => 23, minute => 59);
3516             my $d1= $datedue->clone->set_time_zone('floating');
3517             if ( DateTime->compare( $d1, $expiry_dt ) == 1 ) {
3518                 $datedue = $expiry_dt->clone->set_time_zone( C4::Context->tz );
3519             }
3520         }
3521     }
3522
3523     return $datedue;
3524 }
3525
3526
3527 =head2 CheckRepeatableHolidays
3528
3529   $countrepeatable = CheckRepeatableHoliday($itemnumber,$week_day,$branchcode);
3530
3531 This function checks if the date due is a repeatable holiday
3532
3533 C<$date_due>   = returndate calculate with no day check
3534 C<$itemnumber>  = itemnumber
3535 C<$branchcode>  = localisation of issue 
3536
3537 =cut
3538
3539 sub CheckRepeatableHolidays{
3540 my($itemnumber,$week_day,$branchcode)=@_;
3541 my $dbh = C4::Context->dbh;
3542 my $query = qq|SELECT count(*)  
3543         FROM repeatable_holidays 
3544         WHERE branchcode=?
3545         AND weekday=?|;
3546 my $sth = $dbh->prepare($query);
3547 $sth->execute($branchcode,$week_day);
3548 my $result=$sth->fetchrow;
3549 return $result;
3550 }
3551
3552
3553 =head2 CheckSpecialHolidays
3554
3555   $countspecial = CheckSpecialHolidays($years,$month,$day,$itemnumber,$branchcode);
3556
3557 This function check if the date is a special holiday
3558
3559 C<$years>   = the years of datedue
3560 C<$month>   = the month of datedue
3561 C<$day>     = the day of datedue
3562 C<$itemnumber>  = itemnumber
3563 C<$branchcode>  = localisation of issue 
3564
3565 =cut
3566
3567 sub CheckSpecialHolidays{
3568 my ($years,$month,$day,$itemnumber,$branchcode) = @_;
3569 my $dbh = C4::Context->dbh;
3570 my $query=qq|SELECT count(*) 
3571              FROM `special_holidays`
3572              WHERE year=?
3573              AND month=?
3574              AND day=?
3575              AND branchcode=?
3576             |;
3577 my $sth = $dbh->prepare($query);
3578 $sth->execute($years,$month,$day,$branchcode);
3579 my $countspecial=$sth->fetchrow ;
3580 return $countspecial;
3581 }
3582
3583 =head2 CheckRepeatableSpecialHolidays
3584
3585   $countspecial = CheckRepeatableSpecialHolidays($month,$day,$itemnumber,$branchcode);
3586
3587 This function check if the date is a repeatble special holidays
3588
3589 C<$month>   = the month of datedue
3590 C<$day>     = the day of datedue
3591 C<$itemnumber>  = itemnumber
3592 C<$branchcode>  = localisation of issue 
3593
3594 =cut
3595
3596 sub CheckRepeatableSpecialHolidays{
3597 my ($month,$day,$itemnumber,$branchcode) = @_;
3598 my $dbh = C4::Context->dbh;
3599 my $query=qq|SELECT count(*) 
3600              FROM `repeatable_holidays`
3601              WHERE month=?
3602              AND day=?
3603              AND branchcode=?
3604             |;
3605 my $sth = $dbh->prepare($query);
3606 $sth->execute($month,$day,$branchcode);
3607 my $countspecial=$sth->fetchrow ;
3608 return $countspecial;
3609 }
3610
3611
3612
3613 sub CheckValidBarcode{
3614 my ($barcode) = @_;
3615 my $dbh = C4::Context->dbh;
3616 my $query=qq|SELECT count(*) 
3617              FROM items 
3618              WHERE barcode=?
3619             |;
3620 my $sth = $dbh->prepare($query);
3621 $sth->execute($barcode);
3622 my $exist=$sth->fetchrow ;
3623 return $exist;
3624 }
3625
3626 =head2 IsBranchTransferAllowed
3627
3628   $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3629
3630 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3631
3632 =cut
3633
3634 sub IsBranchTransferAllowed {
3635         my ( $toBranch, $fromBranch, $code ) = @_;
3636
3637         if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3638         
3639         my $limitType = C4::Context->preference("BranchTransferLimitsType");   
3640         my $dbh = C4::Context->dbh;
3641             
3642         my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3643         $sth->execute( $toBranch, $fromBranch, $code );
3644         my $limit = $sth->fetchrow_hashref();
3645                         
3646         ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3647         if ( $limit->{'limitId'} ) {
3648                 return 0;
3649         } else {
3650                 return 1;
3651         }
3652 }                                                        
3653
3654 =head2 CreateBranchTransferLimit
3655
3656   CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3657
3658 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3659
3660 =cut
3661
3662 sub CreateBranchTransferLimit {
3663    my ( $toBranch, $fromBranch, $code ) = @_;
3664    return unless defined($toBranch) && defined($fromBranch);
3665    my $limitType = C4::Context->preference("BranchTransferLimitsType");
3666    
3667    my $dbh = C4::Context->dbh;
3668    
3669    my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3670    return $sth->execute( $code, $toBranch, $fromBranch );
3671 }
3672
3673 =head2 DeleteBranchTransferLimits
3674
3675     my $result = DeleteBranchTransferLimits($frombranch);
3676
3677 Deletes all the library transfer limits for one library.  Returns the
3678 number of limits deleted, 0e0 if no limits were deleted, or undef if
3679 no arguments are supplied.
3680
3681 =cut
3682
3683 sub DeleteBranchTransferLimits {
3684     my $branch = shift;
3685     return unless defined $branch;
3686     my $dbh    = C4::Context->dbh;
3687     my $sth    = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3688     return $sth->execute($branch);
3689 }
3690
3691 sub ReturnLostItem{
3692     my ( $borrowernumber, $itemnum ) = @_;
3693
3694     MarkIssueReturned( $borrowernumber, $itemnum );
3695     my $borrower = C4::Members::GetMember( 'borrowernumber'=>$borrowernumber );
3696     my $item = C4::Items::GetItem( $itemnum );
3697     my $old_note = ($item->{'paidfor'} && ($item->{'paidfor'} ne q{})) ? $item->{'paidfor'}.' / ' : q{};
3698     my @datearr = localtime(time);
3699     my $date = ( 1900 + $datearr[5] ) . "-" . ( $datearr[4] + 1 ) . "-" . $datearr[3];
3700     my $bor = "$borrower->{'firstname'} $borrower->{'surname'} $borrower->{'cardnumber'}";
3701     ModItem({ paidfor =>  $old_note."Paid for by $bor $date" }, undef, $itemnum);
3702 }
3703
3704
3705 sub LostItem{
3706     my ($itemnumber, $mark_returned) = @_;
3707
3708     my $dbh = C4::Context->dbh();
3709     my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title 
3710                            FROM issues 
3711                            JOIN items USING (itemnumber) 
3712                            JOIN biblio USING (biblionumber)
3713                            WHERE issues.itemnumber=?");
3714     $sth->execute($itemnumber);
3715     my $issues=$sth->fetchrow_hashref();
3716
3717     # If a borrower lost the item, add a replacement cost to the their record
3718     if ( my $borrowernumber = $issues->{borrowernumber} ){
3719         my $borrower = C4::Members::GetMemberDetails( $borrowernumber );
3720
3721         if (C4::Context->preference('WhenLostForgiveFine')){
3722             my $fix = _FixOverduesOnReturn($borrowernumber, $itemnumber, 1, 0); # 1, 0 = exemptfine, no-dropbox
3723             defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $itemnumber...) failed!";  # zero is OK, check defined
3724         }
3725         if (C4::Context->preference('WhenLostChargeReplacementFee')){
3726             C4::Accounts::chargelostitem($borrowernumber, $itemnumber, $issues->{'replacementprice'}, "Lost Item $issues->{'title'} $issues->{'barcode'}");
3727             #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3728             #warn " $issues->{'borrowernumber'}  /  $itemnumber ";
3729         }
3730
3731         MarkIssueReturned($borrowernumber,$itemnumber,undef,undef,$borrower->{'privacy'}) if $mark_returned;
3732     }
3733 }
3734
3735 sub GetOfflineOperations {
3736     my $dbh = C4::Context->dbh;
3737     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3738     $sth->execute(C4::Context->userenv->{'branch'});
3739     my $results = $sth->fetchall_arrayref({});
3740     return $results;
3741 }
3742
3743 sub GetOfflineOperation {
3744     my $operationid = shift;
3745     return unless $operationid;
3746     my $dbh = C4::Context->dbh;
3747     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3748     $sth->execute( $operationid );
3749     return $sth->fetchrow_hashref;
3750 }
3751
3752 sub AddOfflineOperation {
3753     my ( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount ) = @_;
3754     my $dbh = C4::Context->dbh;
3755     my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber, amount) VALUES(?,?,?,?,?,?,?)");
3756     $sth->execute( $userid, $branchcode, $timestamp, $action, $barcode, $cardnumber, $amount );
3757     return "Added.";
3758 }
3759
3760 sub DeleteOfflineOperation {
3761     my $dbh = C4::Context->dbh;
3762     my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3763     $sth->execute( shift );
3764     return "Deleted.";
3765 }
3766
3767 sub ProcessOfflineOperation {
3768     my $operation = shift;
3769
3770     my $report;
3771     if ( $operation->{action} eq 'return' ) {
3772         $report = ProcessOfflineReturn( $operation );
3773     } elsif ( $operation->{action} eq 'issue' ) {
3774         $report = ProcessOfflineIssue( $operation );
3775     } elsif ( $operation->{action} eq 'payment' ) {
3776         $report = ProcessOfflinePayment( $operation );
3777     }
3778
3779     DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3780
3781     return $report;
3782 }
3783
3784 sub ProcessOfflineReturn {
3785     my $operation = shift;
3786
3787     my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3788
3789     if ( $itemnumber ) {
3790         my $issue = GetOpenIssue( $itemnumber );
3791         if ( $issue ) {
3792             MarkIssueReturned(
3793                 $issue->{borrowernumber},
3794                 $itemnumber,
3795                 undef,
3796                 $operation->{timestamp},
3797             );
3798             ModItem(
3799                 { renewals => 0, onloan => undef },
3800                 $issue->{'biblionumber'},
3801                 $itemnumber
3802             );
3803             return "Success.";
3804         } else {
3805             return "Item not issued.";
3806         }
3807     } else {
3808         return "Item not found.";
3809     }
3810 }
3811
3812 sub ProcessOfflineIssue {
3813     my $operation = shift;
3814
3815     my $borrower = C4::Members::GetMemberDetails( undef, $operation->{cardnumber} ); # Get borrower from operation cardnumber
3816
3817     if ( $borrower->{borrowernumber} ) {
3818         my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3819         unless ($itemnumber) {
3820             return "Barcode not found.";
3821         }
3822         my $issue = GetOpenIssue( $itemnumber );
3823
3824         if ( $issue and ( $issue->{borrowernumber} ne $borrower->{borrowernumber} ) ) { # Item already issued to another borrower, mark it returned
3825             MarkIssueReturned(
3826                 $issue->{borrowernumber},
3827                 $itemnumber,
3828                 undef,
3829                 $operation->{timestamp},
3830             );
3831         }
3832         AddIssue(
3833             $borrower,
3834             $operation->{'barcode'},
3835             undef,
3836             1,
3837             $operation->{timestamp},
3838             undef,
3839         );
3840         return "Success.";
3841     } else {
3842         return "Borrower not found.";
3843     }
3844 }
3845
3846 sub ProcessOfflinePayment {
3847     my $operation = shift;
3848
3849     my $borrower = C4::Members::GetMemberDetails( undef, $operation->{cardnumber} ); # Get borrower from operation cardnumber
3850     my $amount = $operation->{amount};
3851
3852     recordpayment( $borrower->{borrowernumber}, $amount );
3853
3854     return "Success."
3855 }
3856
3857
3858 =head2 TransferSlip
3859
3860   TransferSlip($user_branch, $itemnumber, $barcode, $to_branch)
3861
3862   Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
3863
3864 =cut
3865
3866 sub TransferSlip {
3867     my ($branch, $itemnumber, $barcode, $to_branch) = @_;
3868
3869     my $item =  GetItem( $itemnumber, $barcode )
3870       or return;
3871
3872     my $pulldate = C4::Dates->new();
3873
3874     return C4::Letters::GetPreparedLetter (
3875         module => 'circulation',
3876         letter_code => 'TRANSFERSLIP',
3877         branchcode => $branch,
3878         tables => {
3879             'branches'    => $to_branch,
3880             'biblio'      => $item->{biblionumber},
3881             'items'       => $item,
3882         },
3883     );
3884 }
3885
3886 =head2 CheckIfIssuedToPatron
3887
3888   CheckIfIssuedToPatron($borrowernumber, $biblionumber)
3889
3890   Return 1 if any record item is issued to patron, otherwise return 0
3891
3892 =cut
3893
3894 sub CheckIfIssuedToPatron {
3895     my ($borrowernumber, $biblionumber) = @_;
3896
3897     my $dbh = C4::Context->dbh;
3898     my $query = q|
3899         SELECT COUNT(*) FROM issues
3900         LEFT JOIN items ON items.itemnumber = issues.itemnumber
3901         WHERE items.biblionumber = ?
3902         AND issues.borrowernumber = ?
3903     |;
3904     my $is_issued = $dbh->selectrow_array($query, {}, $biblionumber, $borrowernumber );
3905     return 1 if $is_issued;
3906     return;
3907 }
3908
3909 =head2 IsItemIssued
3910
3911   IsItemIssued( $itemnumber )
3912
3913   Return 1 if the item is on loan, otherwise return 0
3914
3915 =cut
3916
3917 sub IsItemIssued {
3918     my $itemnumber = shift;
3919     my $dbh = C4::Context->dbh;
3920     my $sth = $dbh->prepare(q{
3921         SELECT COUNT(*)
3922         FROM issues
3923         WHERE itemnumber = ?
3924     });
3925     $sth->execute($itemnumber);
3926     return $sth->fetchrow;
3927 }
3928
3929 =head2 GetAgeRestriction
3930
3931   my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions, $borrower);
3932   my ($ageRestriction, $daysToAgeRestriction) = GetAgeRestriction($record_restrictions);
3933
3934   if($daysToAgeRestriction <= 0) { #Borrower is allowed to access this material, as he is older or as old as the agerestriction }
3935   if($daysToAgeRestriction > 0) { #Borrower is this many days from meeting the agerestriction }
3936
3937 @PARAM1 the koha.biblioitems.agerestriction value, like K18, PEGI 13, ...
3938 @PARAM2 a borrower-object with koha.borrowers.dateofbirth. (OPTIONAL)
3939 @RETURNS The age restriction age in years and the days to fulfill the age restriction for the given borrower.
3940          Negative days mean the borrower has gone past the age restriction age.
3941
3942 =cut
3943
3944 sub GetAgeRestriction {
3945     my ($record_restrictions, $borrower) = @_;
3946     my $markers = C4::Context->preference('AgeRestrictionMarker');
3947
3948     # Split $record_restrictions to something like FSK 16 or PEGI 6
3949     my @values = split ' ', uc($record_restrictions);
3950     return unless @values;
3951
3952     # Search first occurrence of one of the markers
3953     my @markers = split /\|/, uc($markers);
3954     return unless @markers;
3955
3956     my $index            = 0;
3957     my $restriction_year = 0;
3958     for my $value (@values) {
3959         $index++;
3960         for my $marker (@markers) {
3961             $marker =~ s/^\s+//;    #remove leading spaces
3962             $marker =~ s/\s+$//;    #remove trailing spaces
3963             if ( $marker eq $value ) {
3964                 if ( $index <= $#values ) {
3965                     $restriction_year += $values[$index];
3966                 }
3967                 last;
3968             }
3969             elsif ( $value =~ /^\Q$marker\E(\d+)$/ ) {
3970
3971                 # Perhaps it is something like "K16" (as in Finland)
3972                 $restriction_year += $1;
3973                 last;
3974             }
3975         }
3976         last if ( $restriction_year > 0 );
3977     }
3978
3979     #Check if the borrower is age restricted for this material and for how long.
3980     if ($restriction_year && $borrower) {
3981         if ( $borrower->{'dateofbirth'} ) {
3982             my @alloweddate = split /-/, $borrower->{'dateofbirth'};
3983             $alloweddate[0] += $restriction_year;
3984
3985             #Prevent runime eror on leap year (invalid date)
3986             if ( ( $alloweddate[1] == 2 ) && ( $alloweddate[2] == 29 ) ) {
3987                 $alloweddate[2] = 28;
3988             }
3989
3990             #Get how many days the borrower has to reach the age restriction
3991             my $daysToAgeRestriction = Date_to_Days(@alloweddate) - Date_to_Days(Today);
3992             #Negative days means the borrower went past the age restriction age
3993             return ($restriction_year, $daysToAgeRestriction);
3994         }
3995     }
3996
3997     return ($restriction_year);
3998 }
3999
4000 1;
4001
4002 =head2 GetPendingOnSiteCheckouts
4003
4004 =cut
4005
4006 sub GetPendingOnSiteCheckouts {
4007     my $dbh = C4::Context->dbh;
4008     return $dbh->selectall_arrayref(q|
4009         SELECT
4010           items.barcode,
4011           items.biblionumber,
4012           items.itemnumber,
4013           items.itemnotes,
4014           items.itemcallnumber,
4015           items.location,
4016           issues.date_due,
4017           issues.branchcode,
4018           issues.date_due < NOW() AS is_overdue,
4019           biblio.author,
4020           biblio.title,
4021           borrowers.firstname,
4022           borrowers.surname,
4023           borrowers.cardnumber,
4024           borrowers.borrowernumber
4025         FROM items
4026         LEFT JOIN issues ON items.itemnumber = issues.itemnumber
4027         LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber
4028         LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
4029         WHERE issues.onsite_checkout = 1
4030     |, { Slice => {} } );
4031 }
4032
4033 __END__
4034
4035 =head1 AUTHOR
4036
4037 Koha Development Team <http://koha-community.org/>
4038
4039 =cut
4040