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