Bug 9367: Followup: Code optimization: CheckReserves is too often called
[koha.git] / C4 / Circulation.pm
1 package C4::Circulation;
2
3 # Copyright 2000-2002 Katipo Communications
4 # copyright 2010 BibLibre
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it 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         if ( ( $data1->{renewalsallowed} && $data1->{renewalsallowed} > $data1->{renewals} ) || $override_limit ) {
2477             $renewokay = 1;
2478         }
2479         else {
2480             $error = "too_many";
2481         }
2482
2483         my $resstatus = C4::Reserves::GetReserveStatus($itemnumber);
2484         if ( $resstatus eq "Waiting" or $resstatus eq "Reserved" ) {
2485             $renewokay = 0;
2486             $error = "on_reserve";
2487         }
2488     }
2489     return ($renewokay,$error);
2490 }
2491
2492 =head2 AddRenewal
2493
2494   &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
2495
2496 Renews a loan.
2497
2498 C<$borrowernumber> is the borrower number of the patron who currently
2499 has the item.
2500
2501 C<$itemnumber> is the number of the item to renew.
2502
2503 C<$branch> is the library where the renewal took place (if any).
2504            The library that controls the circ policies for the renewal is retrieved from the issues record.
2505
2506 C<$datedue> can be a C4::Dates object used to set the due date.
2507
2508 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate.  If
2509 this parameter is not supplied, lastreneweddate is set to the current date.
2510
2511 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2512 from the book's item type.
2513
2514 =cut
2515
2516 sub AddRenewal {
2517     my $borrowernumber  = shift or return;
2518     my $itemnumber      = shift or return;
2519     my $branch          = shift;
2520     my $datedue         = shift;
2521     my $lastreneweddate = shift || DateTime->now(time_zone => C4::Context->tz)->ymd();
2522     my $item   = GetItem($itemnumber) or return;
2523     my $biblio = GetBiblioFromItemNumber($itemnumber) or return;
2524
2525     my $dbh = C4::Context->dbh;
2526     # Find the issues record for this book
2527     my $sth =
2528       $dbh->prepare("SELECT * FROM issues
2529                         WHERE borrowernumber=? 
2530                         AND itemnumber=?"
2531       );
2532     $sth->execute( $borrowernumber, $itemnumber );
2533     my $issuedata = $sth->fetchrow_hashref;
2534     $sth->finish;
2535     if(defined $datedue && ref $datedue ne 'DateTime' ) {
2536         carp 'Invalid date passed to AddRenewal.';
2537         return;
2538     }
2539     # If the due date wasn't specified, calculate it by adding the
2540     # book's loan length to today's date or the current due date
2541     # based on the value of the RenewalPeriodBase syspref.
2542     unless ($datedue) {
2543
2544         my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber ) or return;
2545         my $itemtype = (C4::Context->preference('item-level_itypes')) ? $biblio->{'itype'} : $biblio->{'itemtype'};
2546
2547         $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
2548                                         $issuedata->{date_due} :
2549                                         DateTime->now( time_zone => C4::Context->tz());
2550         $datedue =  CalcDateDue($datedue,$itemtype,$issuedata->{'branchcode'},$borrower);
2551     }
2552
2553     # Update the issues record to have the new due date, and a new count
2554     # of how many times it has been renewed.
2555     my $renews = $issuedata->{'renewals'} + 1;
2556     $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
2557                             WHERE borrowernumber=? 
2558                             AND itemnumber=?"
2559     );
2560
2561     $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
2562     $sth->finish;
2563
2564     # Update the renewal count on the item, and tell zebra to reindex
2565     $renews = $biblio->{'renewals'} + 1;
2566     ModItem({ renewals => $renews, onloan => $datedue->strftime('%Y-%m-%d %H:%M')}, $biblio->{'biblionumber'}, $itemnumber);
2567
2568     # Charge a new rental fee, if applicable?
2569     my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
2570     if ( $charge > 0 ) {
2571         my $accountno = getnextacctno( $borrowernumber );
2572         my $item = GetBiblioFromItemNumber($itemnumber);
2573         my $manager_id = 0;
2574         $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv; 
2575         $sth = $dbh->prepare(
2576                 "INSERT INTO accountlines
2577                     (date, borrowernumber, accountno, amount, manager_id,
2578                     description,accounttype, amountoutstanding, itemnumber)
2579                     VALUES (now(),?,?,?,?,?,?,?,?)"
2580         );
2581         $sth->execute( $borrowernumber, $accountno, $charge, $manager_id,
2582             "Renewal of Rental Item $item->{'title'} $item->{'barcode'}",
2583             'Rent', $charge, $itemnumber );
2584     }
2585
2586     # Send a renewal slip according to checkout alert preferencei
2587     if ( C4::Context->preference('RenewalSendNotice') eq '1') {
2588         my $borrower = C4::Members::GetMemberDetails( $borrowernumber, 0 );
2589         my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2590         my %conditions = (
2591                 branchcode   => $branch,
2592                 categorycode => $borrower->{categorycode},
2593                 item_type    => $item->{itype},
2594                 notification => 'CHECKOUT',
2595         );
2596         if ($circulation_alert->is_enabled_for(\%conditions)) {
2597                 SendCirculationAlert({
2598                         type     => 'RENEWAL',
2599                         item     => $item,
2600                 borrower => $borrower,
2601                 branch   => $branch,
2602                 });
2603         }
2604     }
2605
2606     # Log the renewal
2607     UpdateStats( $branch, 'renew', $charge, '', $itemnumber, $item->{itype}, $borrowernumber, undef, $item->{'ccode'});
2608         return $datedue;
2609 }
2610
2611 sub GetRenewCount {
2612     # check renewal status
2613     my ( $bornum, $itemno ) = @_;
2614     my $dbh           = C4::Context->dbh;
2615     my $renewcount    = 0;
2616     my $renewsallowed = 0;
2617     my $renewsleft    = 0;
2618
2619     my $borrower = C4::Members::GetMember( borrowernumber => $bornum);
2620     my $item     = GetItem($itemno); 
2621
2622     # Look in the issues table for this item, lent to this borrower,
2623     # and not yet returned.
2624
2625     # FIXME - I think this function could be redone to use only one SQL call.
2626     my $sth = $dbh->prepare(
2627         "select * from issues
2628                                 where (borrowernumber = ?)
2629                                 and (itemnumber = ?)"
2630     );
2631     $sth->execute( $bornum, $itemno );
2632     my $data = $sth->fetchrow_hashref;
2633     $renewcount = $data->{'renewals'} if $data->{'renewals'};
2634     $sth->finish;
2635     # $item and $borrower should be calculated
2636     my $branchcode = _GetCircControlBranch($item, $borrower);
2637     
2638     my $issuingrule = GetIssuingRule($borrower->{categorycode}, $item->{itype}, $branchcode);
2639     
2640     $renewsallowed = $issuingrule->{'renewalsallowed'};
2641     $renewsleft    = $renewsallowed - $renewcount;
2642     if($renewsleft < 0){ $renewsleft = 0; }
2643     return ( $renewcount, $renewsallowed, $renewsleft );
2644 }
2645
2646 =head2 GetIssuingCharges
2647
2648   ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
2649
2650 Calculate how much it would cost for a given patron to borrow a given
2651 item, including any applicable discounts.
2652
2653 C<$itemnumber> is the item number of item the patron wishes to borrow.
2654
2655 C<$borrowernumber> is the patron's borrower number.
2656
2657 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
2658 and C<$item_type> is the code for the item's item type (e.g., C<VID>
2659 if it's a video).
2660
2661 =cut
2662
2663 sub GetIssuingCharges {
2664
2665     # calculate charges due
2666     my ( $itemnumber, $borrowernumber ) = @_;
2667     my $charge = 0;
2668     my $dbh    = C4::Context->dbh;
2669     my $item_type;
2670
2671     # Get the book's item type and rental charge (via its biblioitem).
2672     my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
2673         LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
2674     $charge_query .= (C4::Context->preference('item-level_itypes'))
2675         ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
2676         : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
2677
2678     $charge_query .= ' WHERE items.itemnumber =?';
2679
2680     my $sth = $dbh->prepare($charge_query);
2681     $sth->execute($itemnumber);
2682     if ( my $item_data = $sth->fetchrow_hashref ) {
2683         $item_type = $item_data->{itemtype};
2684         $charge    = $item_data->{rentalcharge};
2685         my $branch = C4::Branch::mybranch();
2686         my $discount_query = q|SELECT rentaldiscount,
2687             issuingrules.itemtype, issuingrules.branchcode
2688             FROM borrowers
2689             LEFT JOIN issuingrules ON borrowers.categorycode = issuingrules.categorycode
2690             WHERE borrowers.borrowernumber = ?
2691             AND (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
2692             AND (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')|;
2693         my $discount_sth = $dbh->prepare($discount_query);
2694         $discount_sth->execute( $borrowernumber, $item_type, $branch );
2695         my $discount_rules = $discount_sth->fetchall_arrayref({});
2696         if (@{$discount_rules}) {
2697             # We may have multiple rules so get the most specific
2698             my $discount = _get_discount_from_rule($discount_rules, $branch, $item_type);
2699             $charge = ( $charge * ( 100 - $discount ) ) / 100;
2700         }
2701     }
2702
2703     $sth->finish; # we havent _explicitly_ fetched all rows
2704     return ( $charge, $item_type );
2705 }
2706
2707 # Select most appropriate discount rule from those returned
2708 sub _get_discount_from_rule {
2709     my ($rules_ref, $branch, $itemtype) = @_;
2710     my $discount;
2711
2712     if (@{$rules_ref} == 1) { # only 1 applicable rule use it
2713         $discount = $rules_ref->[0]->{rentaldiscount};
2714         return (defined $discount) ? $discount : 0;
2715     }
2716     # could have up to 4 does one match $branch and $itemtype
2717     my @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq $itemtype } @{$rules_ref};
2718     if (@d) {
2719         $discount = $d[0]->{rentaldiscount};
2720         return (defined $discount) ? $discount : 0;
2721     }
2722     # do we have item type + all branches
2723     @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq $itemtype } @{$rules_ref};
2724     if (@d) {
2725         $discount = $d[0]->{rentaldiscount};
2726         return (defined $discount) ? $discount : 0;
2727     }
2728     # do we all item types + this branch
2729     @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq q{*} } @{$rules_ref};
2730     if (@d) {
2731         $discount = $d[0]->{rentaldiscount};
2732         return (defined $discount) ? $discount : 0;
2733     }
2734     # so all and all (surely we wont get here)
2735     @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq q{*} } @{$rules_ref};
2736     if (@d) {
2737         $discount = $d[0]->{rentaldiscount};
2738         return (defined $discount) ? $discount : 0;
2739     }
2740     # none of the above
2741     return 0;
2742 }
2743
2744 =head2 AddIssuingCharge
2745
2746   &AddIssuingCharge( $itemno, $borrowernumber, $charge )
2747
2748 =cut
2749
2750 sub AddIssuingCharge {
2751     my ( $itemnumber, $borrowernumber, $charge ) = @_;
2752     my $dbh = C4::Context->dbh;
2753     my $nextaccntno = getnextacctno( $borrowernumber );
2754     my $manager_id = 0;
2755     $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
2756     my $query ="
2757         INSERT INTO accountlines
2758             (borrowernumber, itemnumber, accountno,
2759             date, amount, description, accounttype,
2760             amountoutstanding, manager_id)
2761         VALUES (?, ?, ?,now(), ?, 'Rental', 'Rent',?,?)
2762     ";
2763     my $sth = $dbh->prepare($query);
2764     $sth->execute( $borrowernumber, $itemnumber, $nextaccntno, $charge, $charge, $manager_id );
2765     $sth->finish;
2766 }
2767
2768 =head2 GetTransfers
2769
2770   GetTransfers($itemnumber);
2771
2772 =cut
2773
2774 sub GetTransfers {
2775     my ($itemnumber) = @_;
2776
2777     my $dbh = C4::Context->dbh;
2778
2779     my $query = '
2780         SELECT datesent,
2781                frombranch,
2782                tobranch
2783         FROM branchtransfers
2784         WHERE itemnumber = ?
2785           AND datearrived IS NULL
2786         ';
2787     my $sth = $dbh->prepare($query);
2788     $sth->execute($itemnumber);
2789     my @row = $sth->fetchrow_array();
2790     $sth->finish;
2791     return @row;
2792 }
2793
2794 =head2 GetTransfersFromTo
2795
2796   @results = GetTransfersFromTo($frombranch,$tobranch);
2797
2798 Returns the list of pending transfers between $from and $to branch
2799
2800 =cut
2801
2802 sub GetTransfersFromTo {
2803     my ( $frombranch, $tobranch ) = @_;
2804     return unless ( $frombranch && $tobranch );
2805     my $dbh   = C4::Context->dbh;
2806     my $query = "
2807         SELECT itemnumber,datesent,frombranch
2808         FROM   branchtransfers
2809         WHERE  frombranch=?
2810           AND  tobranch=?
2811           AND datearrived IS NULL
2812     ";
2813     my $sth = $dbh->prepare($query);
2814     $sth->execute( $frombranch, $tobranch );
2815     my @gettransfers;
2816
2817     while ( my $data = $sth->fetchrow_hashref ) {
2818         push @gettransfers, $data;
2819     }
2820     $sth->finish;
2821     return (@gettransfers);
2822 }
2823
2824 =head2 DeleteTransfer
2825
2826   &DeleteTransfer($itemnumber);
2827
2828 =cut
2829
2830 sub DeleteTransfer {
2831     my ($itemnumber) = @_;
2832     my $dbh          = C4::Context->dbh;
2833     my $sth          = $dbh->prepare(
2834         "DELETE FROM branchtransfers
2835          WHERE itemnumber=?
2836          AND datearrived IS NULL "
2837     );
2838     $sth->execute($itemnumber);
2839     $sth->finish;
2840 }
2841
2842 =head2 AnonymiseIssueHistory
2843
2844   $rows = AnonymiseIssueHistory($date,$borrowernumber)
2845
2846 This function write NULL instead of C<$borrowernumber> given on input arg into the table issues.
2847 if C<$borrowernumber> is not set, it will delete the issue history for all borrower older than C<$date>.
2848
2849 If c<$borrowernumber> is set, it will delete issue history for only that borrower, regardless of their opac privacy
2850 setting (force delete).
2851
2852 return the number of affected rows.
2853
2854 =cut
2855
2856 sub AnonymiseIssueHistory {
2857     my $date           = shift;
2858     my $borrowernumber = shift;
2859     my $dbh            = C4::Context->dbh;
2860     my $query          = "
2861         UPDATE old_issues
2862         SET    borrowernumber = ?
2863         WHERE  returndate < ?
2864           AND borrowernumber IS NOT NULL
2865     ";
2866
2867     # The default of 0 does not work due to foreign key constraints
2868     # The anonymisation will fail quietly if AnonymousPatron is not a valid entry
2869     my $anonymouspatron = (C4::Context->preference('AnonymousPatron')) ? C4::Context->preference('AnonymousPatron') : 0;
2870     my @bind_params = ($anonymouspatron, $date);
2871     if (defined $borrowernumber) {
2872        $query .= " AND borrowernumber = ?";
2873        push @bind_params, $borrowernumber;
2874     } else {
2875        $query .= " AND (SELECT privacy FROM borrowers WHERE borrowers.borrowernumber=old_issues.borrowernumber) <> 0";
2876     }
2877     my $sth = $dbh->prepare($query);
2878     $sth->execute(@bind_params);
2879     my $rows_affected = $sth->rows;  ### doublecheck row count return function
2880     return $rows_affected;
2881 }
2882
2883 =head2 SendCirculationAlert
2884
2885 Send out a C<check-in> or C<checkout> alert using the messaging system.
2886
2887 B<Parameters>:
2888
2889 =over 4
2890
2891 =item type
2892
2893 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
2894
2895 =item item
2896
2897 Hashref of information about the item being checked in or out.
2898
2899 =item borrower
2900
2901 Hashref of information about the borrower of the item.
2902
2903 =item branch
2904
2905 The branchcode from where the checkout or check-in took place.
2906
2907 =back
2908
2909 B<Example>:
2910
2911     SendCirculationAlert({
2912         type     => 'CHECKOUT',
2913         item     => $item,
2914         borrower => $borrower,
2915         branch   => $branch,
2916     });
2917
2918 =cut
2919
2920 sub SendCirculationAlert {
2921     my ($opts) = @_;
2922     my ($type, $item, $borrower, $branch) =
2923         ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
2924     my %message_name = (
2925         CHECKIN  => 'Item_Check_in',
2926         CHECKOUT => 'Item_Checkout',
2927         RENEWAL  => 'Item_Checkout',
2928     );
2929     my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
2930         borrowernumber => $borrower->{borrowernumber},
2931         message_name   => $message_name{$type},
2932     });
2933     my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues';
2934     my $letter =  C4::Letters::GetPreparedLetter (
2935         module => 'circulation',
2936         letter_code => $type,
2937         branchcode => $branch,
2938         tables => {
2939             $issues_table => $item->{itemnumber},
2940             'items'       => $item->{itemnumber},
2941             'biblio'      => $item->{biblionumber},
2942             'biblioitems' => $item->{biblionumber},
2943             'borrowers'   => $borrower,
2944             'branches'    => $branch,
2945         }
2946     ) or return;
2947
2948     my @transports = keys %{ $borrower_preferences->{transports} };
2949     # warn "no transports" unless @transports;
2950     for (@transports) {
2951         # warn "transport: $_";
2952         my $message = C4::Message->find_last_message($borrower, $type, $_);
2953         if (!$message) {
2954             #warn "create new message";
2955             C4::Message->enqueue($letter, $borrower, $_);
2956         } else {
2957             #warn "append to old message";
2958             $message->append($letter);
2959             $message->update;
2960         }
2961     }
2962
2963     return $letter;
2964 }
2965
2966 =head2 updateWrongTransfer
2967
2968   $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
2969
2970 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 
2971
2972 =cut
2973
2974 sub updateWrongTransfer {
2975         my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
2976         my $dbh = C4::Context->dbh;     
2977 # first step validate the actual line of transfert .
2978         my $sth =
2979                 $dbh->prepare(
2980                         "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
2981                 );
2982                 $sth->execute($FromLibrary,$itemNumber);
2983                 $sth->finish;
2984
2985 # second step create a new line of branchtransfer to the right location .
2986         ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
2987
2988 #third step changing holdingbranch of item
2989         UpdateHoldingbranch($FromLibrary,$itemNumber);
2990 }
2991
2992 =head2 UpdateHoldingbranch
2993
2994   $items = UpdateHoldingbranch($branch,$itmenumber);
2995
2996 Simple methode for updating hodlingbranch in items BDD line
2997
2998 =cut
2999
3000 sub UpdateHoldingbranch {
3001         my ( $branch,$itemnumber ) = @_;
3002     ModItem({ holdingbranch => $branch }, undef, $itemnumber);
3003 }
3004
3005 =head2 CalcDateDue
3006
3007 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
3008
3009 this function calculates the due date given the start date and configured circulation rules,
3010 checking against the holidays calendar as per the 'useDaysMode' syspref.
3011 C<$startdate>   = C4::Dates object representing start date of loan period (assumed to be today)
3012 C<$itemtype>  = itemtype code of item in question
3013 C<$branch>  = location whose calendar to use
3014 C<$borrower> = Borrower object
3015
3016 =cut
3017
3018 sub CalcDateDue {
3019     my ( $startdate, $itemtype, $branch, $borrower ) = @_;
3020
3021     # loanlength now a href
3022     my $loanlength =
3023       GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
3024
3025     my $datedue;
3026
3027     # if globalDueDate ON the datedue is set to that date
3028     if (C4::Context->preference('globalDueDate')
3029         && ( C4::Context->preference('globalDueDate') =~
3030             C4::Dates->regexp('syspref') )
3031       ) {
3032         $datedue = dt_from_string(
3033             C4::Context->preference('globalDueDate'),
3034             C4::Context->preference('dateformat')
3035         );
3036     } else {
3037
3038         # otherwise, calculate the datedue as normal
3039         if ( C4::Context->preference('useDaysMode') eq 'Days' )
3040         {    # ignoring calendar
3041             my $dt =
3042               DateTime->now( time_zone => C4::Context->tz() )
3043               ->truncate( to => 'minute' );
3044             if ( $loanlength->{lengthunit} eq 'hours' ) {
3045                 $dt->add( hours => $loanlength->{issuelength} );
3046             } else {    # days
3047                 $dt->add( days => $loanlength->{issuelength} );
3048                 $dt->set_hour(23);
3049                 $dt->set_minute(59);
3050             }
3051             # break
3052             return $dt;
3053
3054         } else {
3055             my $dur;
3056             if ($loanlength->{lengthunit} eq 'hours') {
3057                 $dur = DateTime::Duration->new( hours => $loanlength->{issuelength});
3058             }
3059             else { # days
3060                 $dur = DateTime::Duration->new( days => $loanlength->{issuelength});
3061             }
3062             if (ref $startdate ne 'DateTime' ) {
3063                 $startdate = dt_from_string($startdate);
3064             }
3065             my $calendar = Koha::Calendar->new( branchcode => $branch );
3066             $datedue = $calendar->addDate( $startdate, $dur, $loanlength->{lengthunit} );
3067             if ($loanlength->{lengthunit} eq 'days') {
3068                 $datedue->set_hour(23);
3069                 $datedue->set_minute(59);
3070             }
3071         }
3072     }
3073
3074     # if Hard Due Dates are used, retreive them and apply as necessary
3075     my ( $hardduedate, $hardduedatecompare ) =
3076       GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
3077     if ($hardduedate) {    # hardduedates are currently dates
3078         $hardduedate->truncate( to => 'minute' );
3079         $hardduedate->set_hour(23);
3080         $hardduedate->set_minute(59);
3081         my $cmp = DateTime->compare( $hardduedate, $datedue );
3082
3083 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
3084 # if the calculated date is before the 'after' Hard Due Date (floor), override
3085 # if the hard due date is set to 'exactly', overrride
3086         if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
3087             $datedue = $hardduedate->clone;
3088         }
3089
3090         # in all other cases, keep the date due as it is
3091     }
3092
3093     # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3094     if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3095         my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso' );
3096         $expiry_dt->set( hour => 23, minute => 59);
3097         if ( DateTime->compare( $datedue, $expiry_dt ) == 1 ) {
3098             $datedue = $expiry_dt->clone;
3099         }
3100     }
3101
3102     return $datedue;
3103 }
3104
3105
3106 =head2 CheckRepeatableHolidays
3107
3108   $countrepeatable = CheckRepeatableHoliday($itemnumber,$week_day,$branchcode);
3109
3110 This function checks if the date due is a repeatable holiday
3111
3112 C<$date_due>   = returndate calculate with no day check
3113 C<$itemnumber>  = itemnumber
3114 C<$branchcode>  = localisation of issue 
3115
3116 =cut
3117
3118 sub CheckRepeatableHolidays{
3119 my($itemnumber,$week_day,$branchcode)=@_;
3120 my $dbh = C4::Context->dbh;
3121 my $query = qq|SELECT count(*)  
3122         FROM repeatable_holidays 
3123         WHERE branchcode=?
3124         AND weekday=?|;
3125 my $sth = $dbh->prepare($query);
3126 $sth->execute($branchcode,$week_day);
3127 my $result=$sth->fetchrow;
3128 $sth->finish;
3129 return $result;
3130 }
3131
3132
3133 =head2 CheckSpecialHolidays
3134
3135   $countspecial = CheckSpecialHolidays($years,$month,$day,$itemnumber,$branchcode);
3136
3137 This function check if the date is a special holiday
3138
3139 C<$years>   = the years of datedue
3140 C<$month>   = the month of datedue
3141 C<$day>     = the day of datedue
3142 C<$itemnumber>  = itemnumber
3143 C<$branchcode>  = localisation of issue 
3144
3145 =cut
3146
3147 sub CheckSpecialHolidays{
3148 my ($years,$month,$day,$itemnumber,$branchcode) = @_;
3149 my $dbh = C4::Context->dbh;
3150 my $query=qq|SELECT count(*) 
3151              FROM `special_holidays`
3152              WHERE year=?
3153              AND month=?
3154              AND day=?
3155              AND branchcode=?
3156             |;
3157 my $sth = $dbh->prepare($query);
3158 $sth->execute($years,$month,$day,$branchcode);
3159 my $countspecial=$sth->fetchrow ;
3160 $sth->finish;
3161 return $countspecial;
3162 }
3163
3164 =head2 CheckRepeatableSpecialHolidays
3165
3166   $countspecial = CheckRepeatableSpecialHolidays($month,$day,$itemnumber,$branchcode);
3167
3168 This function check if the date is a repeatble special holidays
3169
3170 C<$month>   = the month of datedue
3171 C<$day>     = the day of datedue
3172 C<$itemnumber>  = itemnumber
3173 C<$branchcode>  = localisation of issue 
3174
3175 =cut
3176
3177 sub CheckRepeatableSpecialHolidays{
3178 my ($month,$day,$itemnumber,$branchcode) = @_;
3179 my $dbh = C4::Context->dbh;
3180 my $query=qq|SELECT count(*) 
3181              FROM `repeatable_holidays`
3182              WHERE month=?
3183              AND day=?
3184              AND branchcode=?
3185             |;
3186 my $sth = $dbh->prepare($query);
3187 $sth->execute($month,$day,$branchcode);
3188 my $countspecial=$sth->fetchrow ;
3189 $sth->finish;
3190 return $countspecial;
3191 }
3192
3193
3194
3195 sub CheckValidBarcode{
3196 my ($barcode) = @_;
3197 my $dbh = C4::Context->dbh;
3198 my $query=qq|SELECT count(*) 
3199              FROM items 
3200              WHERE barcode=?
3201             |;
3202 my $sth = $dbh->prepare($query);
3203 $sth->execute($barcode);
3204 my $exist=$sth->fetchrow ;
3205 $sth->finish;
3206 return $exist;
3207 }
3208
3209 =head2 IsBranchTransferAllowed
3210
3211   $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3212
3213 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3214
3215 =cut
3216
3217 sub IsBranchTransferAllowed {
3218         my ( $toBranch, $fromBranch, $code ) = @_;
3219
3220         if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3221         
3222         my $limitType = C4::Context->preference("BranchTransferLimitsType");   
3223         my $dbh = C4::Context->dbh;
3224             
3225         my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3226         $sth->execute( $toBranch, $fromBranch, $code );
3227         my $limit = $sth->fetchrow_hashref();
3228                         
3229         ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3230         if ( $limit->{'limitId'} ) {
3231                 return 0;
3232         } else {
3233                 return 1;
3234         }
3235 }                                                        
3236
3237 =head2 CreateBranchTransferLimit
3238
3239   CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3240
3241 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3242
3243 =cut
3244
3245 sub CreateBranchTransferLimit {
3246    my ( $toBranch, $fromBranch, $code ) = @_;
3247
3248    my $limitType = C4::Context->preference("BranchTransferLimitsType");
3249    
3250    my $dbh = C4::Context->dbh;
3251    
3252    my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3253    $sth->execute( $code, $toBranch, $fromBranch );
3254 }
3255
3256 =head2 DeleteBranchTransferLimits
3257
3258 DeleteBranchTransferLimits($frombranch);
3259
3260 Deletes all the branch transfer limits for one branch
3261
3262 =cut
3263
3264 sub DeleteBranchTransferLimits {
3265     my $branch = shift;
3266     my $dbh    = C4::Context->dbh;
3267     my $sth    = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3268     $sth->execute($branch);
3269 }
3270
3271 sub ReturnLostItem{
3272     my ( $borrowernumber, $itemnum ) = @_;
3273
3274     MarkIssueReturned( $borrowernumber, $itemnum );
3275     my $borrower = C4::Members::GetMember( 'borrowernumber'=>$borrowernumber );
3276     my $item = C4::Items::GetItem( $itemnum );
3277     my $old_note = ($item->{'paidfor'} && ($item->{'paidfor'} ne q{})) ? $item->{'paidfor'}.' / ' : q{};
3278     my @datearr = localtime(time);
3279     my $date = ( 1900 + $datearr[5] ) . "-" . ( $datearr[4] + 1 ) . "-" . $datearr[3];
3280     my $bor = "$borrower->{'firstname'} $borrower->{'surname'} $borrower->{'cardnumber'}";
3281     ModItem({ paidfor =>  $old_note."Paid for by $bor $date" }, undef, $itemnum);
3282 }
3283
3284
3285 sub LostItem{
3286     my ($itemnumber, $mark_returned, $charge_fee) = @_;
3287
3288     my $dbh = C4::Context->dbh();
3289     my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title 
3290                            FROM issues 
3291                            JOIN items USING (itemnumber) 
3292                            JOIN biblio USING (biblionumber)
3293                            WHERE issues.itemnumber=?");
3294     $sth->execute($itemnumber);
3295     my $issues=$sth->fetchrow_hashref();
3296     $sth->finish;
3297
3298     # if a borrower lost the item, add a replacement cost to the their record
3299     if ( my $borrowernumber = $issues->{borrowernumber} ){
3300         my $borrower = C4::Members::GetMemberDetails( $borrowernumber );
3301
3302         C4::Accounts::chargelostitem($borrowernumber, $itemnumber, $issues->{'replacementprice'}, "Lost Item $issues->{'title'} $issues->{'barcode'}")
3303           if $charge_fee;
3304         #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3305         #warn " $issues->{'borrowernumber'}  /  $itemnumber ";
3306         MarkIssueReturned($borrowernumber,$itemnumber,undef,undef,$borrower->{'privacy'}) if $mark_returned;
3307     }
3308 }
3309
3310 sub GetOfflineOperations {
3311     my $dbh = C4::Context->dbh;
3312     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3313     $sth->execute(C4::Context->userenv->{'branch'});
3314     my $results = $sth->fetchall_arrayref({});
3315     $sth->finish;
3316     return $results;
3317 }
3318
3319 sub GetOfflineOperation {
3320     my $dbh = C4::Context->dbh;
3321     my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3322     $sth->execute( shift );
3323     my $result = $sth->fetchrow_hashref;
3324     $sth->finish;
3325     return $result;
3326 }
3327
3328 sub AddOfflineOperation {
3329     my $dbh = C4::Context->dbh;
3330     my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber) VALUES(?,?,?,?,?,?)");
3331     $sth->execute( @_ );
3332     return "Added.";
3333 }
3334
3335 sub DeleteOfflineOperation {
3336     my $dbh = C4::Context->dbh;
3337     my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3338     $sth->execute( shift );
3339     return "Deleted.";
3340 }
3341
3342 sub ProcessOfflineOperation {
3343     my $operation = shift;
3344
3345     my $report;
3346     if ( $operation->{action} eq 'return' ) {
3347         $report = ProcessOfflineReturn( $operation );
3348     } elsif ( $operation->{action} eq 'issue' ) {
3349         $report = ProcessOfflineIssue( $operation );
3350     }
3351
3352     DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3353
3354     return $report;
3355 }
3356
3357 sub ProcessOfflineReturn {
3358     my $operation = shift;
3359
3360     my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3361
3362     if ( $itemnumber ) {
3363         my $issue = GetOpenIssue( $itemnumber );
3364         if ( $issue ) {
3365             MarkIssueReturned(
3366                 $issue->{borrowernumber},
3367                 $itemnumber,
3368                 undef,
3369                 $operation->{timestamp},
3370             );
3371             ModItem(
3372                 { renewals => 0, onloan => undef },
3373                 $issue->{'biblionumber'},
3374                 $itemnumber
3375             );
3376             return "Success.";
3377         } else {
3378             return "Item not issued.";
3379         }
3380     } else {
3381         return "Item not found.";
3382     }
3383 }
3384
3385 sub ProcessOfflineIssue {
3386     my $operation = shift;
3387
3388     my $borrower = C4::Members::GetMemberDetails( undef, $operation->{cardnumber} ); # Get borrower from operation cardnumber
3389
3390     if ( $borrower->{borrowernumber} ) {
3391         my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3392         unless ($itemnumber) {
3393             return "Barcode not found.";
3394         }
3395         my $issue = GetOpenIssue( $itemnumber );
3396
3397         if ( $issue and ( $issue->{borrowernumber} ne $borrower->{borrowernumber} ) ) { # Item already issued to another borrower, mark it returned
3398             MarkIssueReturned(
3399                 $issue->{borrowernumber},
3400                 $itemnumber,
3401                 undef,
3402                 $operation->{timestamp},
3403             );
3404         }
3405         AddIssue(
3406             $borrower,
3407             $operation->{'barcode'},
3408             undef,
3409             1,
3410             $operation->{timestamp},
3411             undef,
3412         );
3413         return "Success.";
3414     } else {
3415         return "Borrower not found.";
3416     }
3417 }
3418
3419
3420
3421 =head2 TransferSlip
3422
3423   TransferSlip($user_branch, $itemnumber, $to_branch)
3424
3425   Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
3426
3427 =cut
3428
3429 sub TransferSlip {
3430     my ($branch, $itemnumber, $to_branch) = @_;
3431
3432     my $item =  GetItem( $itemnumber )
3433       or return;
3434
3435     my $pulldate = C4::Dates->new();
3436
3437     return C4::Letters::GetPreparedLetter (
3438         module => 'circulation',
3439         letter_code => 'TRANSFERSLIP',
3440         branchcode => $branch,
3441         tables => {
3442             'branches'    => $to_branch,
3443             'biblio'      => $item->{biblionumber},
3444             'items'       => $item,
3445         },
3446     );
3447 }
3448
3449 =head2 CheckIfIssuedToPatron
3450
3451   CheckIfIssuedToPatron($borrowernumber, $biblionumber)
3452
3453   Return 1 if any record item is issued to patron, otherwise return 0
3454
3455 =cut
3456
3457 sub CheckIfIssuedToPatron {
3458     my ($borrowernumber, $biblionumber) = @_;
3459
3460     my $items = GetItemsByBiblioitemnumber($biblionumber);
3461
3462     foreach my $item (@{$items}) {
3463         return 1 if ($item->{borrowernumber} && $item->{borrowernumber} eq $borrowernumber);
3464     }
3465
3466     return;
3467 }
3468
3469
3470 1;
3471
3472 __END__
3473
3474 =head1 AUTHOR
3475
3476 Koha Development Team <http://koha-community.org/>
3477
3478 =cut
3479