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