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