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