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