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