Bug 7517: Revised Patch. Patron category types not displaying <optgroup> in dropdown.
[koha.git] / C4 / Circulation.pm
blob1a156d9354edddc3512d5c789971f221a7e0a5f8
1 package C4::Circulation;
3 # Copyright 2000-2002 Katipo Communications
4 # copyright 2010 BibLibre
6 # This file is part of Koha.
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.
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.
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.
22 use strict;
23 #use warnings; FIXME - Bug 2505
24 use C4::Context;
25 use C4::Stats;
26 use C4::Reserves;
27 use C4::Biblio;
28 use C4::Items;
29 use C4::Members;
30 use C4::Dates;
31 use C4::Calendar;
32 use C4::Accounts;
33 use C4::ItemCirculationAlertPreference;
34 use C4::Dates qw(format_date);
35 use C4::Message;
36 use C4::Debug;
37 use Date::Calc qw(
38 Today
39 Today_and_Now
40 Add_Delta_YM
41 Add_Delta_DHMS
42 Date_to_Days
43 Day_of_Week
44 Add_Delta_Days
45 check_date
46 Delta_Days
48 use POSIX qw(strftime);
49 use C4::Branch; # GetBranches
50 use C4::Log; # logaction
52 use Data::Dumper;
54 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
56 BEGIN {
57 require Exporter;
58 $VERSION = 3.02; # for version checking
59 @ISA = qw(Exporter);
61 # FIXME subs that should probably be elsewhere
62 push @EXPORT, qw(
63 &barcodedecode
64 &LostItem
65 &ReturnLostItem
68 # subs to deal with issuing a book
69 push @EXPORT, qw(
70 &CanBookBeIssued
71 &CanBookBeRenewed
72 &AddIssue
73 &AddRenewal
74 &GetRenewCount
75 &GetItemIssue
76 &GetItemIssues
77 &GetIssuingCharges
78 &GetIssuingRule
79 &GetBranchBorrowerCircRule
80 &GetBranchItemRule
81 &GetBiblioIssues
82 &GetOpenIssue
83 &AnonymiseIssueHistory
86 # subs to deal with returns
87 push @EXPORT, qw(
88 &AddReturn
89 &MarkIssueReturned
92 # subs to deal with transfers
93 push @EXPORT, qw(
94 &transferbook
95 &GetTransfers
96 &GetTransfersFromTo
97 &updateWrongTransfer
98 &DeleteTransfer
99 &IsBranchTransferAllowed
100 &CreateBranchTransferLimit
101 &DeleteBranchTransferLimits
104 # subs to deal with offline circulation
105 push @EXPORT, qw(
106 &GetOfflineOperations
107 &GetOfflineOperation
108 &AddOfflineOperation
109 &DeleteOfflineOperation
110 &ProcessOfflineOperation
114 =head1 NAME
116 C4::Circulation - Koha circulation module
118 =head1 SYNOPSIS
120 use C4::Circulation;
122 =head1 DESCRIPTION
124 The functions in this module deal with circulation, issues, and
125 returns, as well as general information about the library.
126 Also deals with stocktaking.
128 =head1 FUNCTIONS
130 =head2 barcodedecode
132 $str = &barcodedecode($barcode, [$filter]);
134 Generic filter function for barcode string.
135 Called on every circ if the System Pref itemBarcodeInputFilter is set.
136 Will do some manipulation of the barcode for systems that deliver a barcode
137 to circulation.pl that differs from the barcode stored for the item.
138 For proper functioning of this filter, calling the function on the
139 correct barcode string (items.barcode) should return an unaltered barcode.
141 The optional $filter argument is to allow for testing or explicit
142 behavior that ignores the System Pref. Valid values are the same as the
143 System Pref options.
145 =cut
147 # FIXME -- the &decode fcn below should be wrapped into this one.
148 # FIXME -- these plugins should be moved out of Circulation.pm
150 sub barcodedecode {
151 my ($barcode, $filter) = @_;
152 my $branch = C4::Branch::mybranch();
153 $filter = C4::Context->preference('itemBarcodeInputFilter') unless $filter;
154 $filter or return $barcode; # ensure filter is defined, else return untouched barcode
155 if ($filter eq 'whitespace') {
156 $barcode =~ s/\s//g;
157 } elsif ($filter eq 'cuecat') {
158 chomp($barcode);
159 my @fields = split( /\./, $barcode );
160 my @results = map( decode($_), @fields[ 1 .. $#fields ] );
161 ($#results == 2) and return $results[2];
162 } elsif ($filter eq 'T-prefix') {
163 if ($barcode =~ /^[Tt](\d)/) {
164 (defined($1) and $1 eq '0') and return $barcode;
165 $barcode = substr($barcode, 2) + 0; # FIXME: probably should be substr($barcode, 1)
167 return sprintf("T%07d", $barcode);
168 # FIXME: $barcode could be "T1", causing warning: substr outside of string
169 # Why drop the nonzero digit after the T?
170 # Why pass non-digits (or empty string) to "T%07d"?
171 } elsif ($filter eq 'libsuite8') {
172 unless($barcode =~ m/^($branch)-/i){ #if barcode starts with branch code its in Koha style. Skip it.
173 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
174 $barcode =~ s/^[0]*(\d+)$/$branch-b-$1/i;
175 }else{
176 $barcode =~ s/^(\D+)[0]*(\d+)$/$branch-$1-$2/i;
180 return $barcode; # return barcode, modified or not
183 =head2 decode
185 $str = &decode($chunk);
187 Decodes a segment of a string emitted by a CueCat barcode scanner and
188 returns it.
190 FIXME: Should be replaced with Barcode::Cuecat from CPAN
191 or Javascript based decoding on the client side.
193 =cut
195 sub decode {
196 my ($encoded) = @_;
197 my $seq =
198 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-';
199 my @s = map { index( $seq, $_ ); } split( //, $encoded );
200 my $l = ( $#s + 1 ) % 4;
201 if ($l) {
202 if ( $l == 1 ) {
203 # warn "Error: Cuecat decode parsing failed!";
204 return;
206 $l = 4 - $l;
207 $#s += $l;
209 my $r = '';
210 while ( $#s >= 0 ) {
211 my $n = ( ( $s[0] << 6 | $s[1] ) << 6 | $s[2] ) << 6 | $s[3];
212 $r .=
213 chr( ( $n >> 16 ) ^ 67 )
214 .chr( ( $n >> 8 & 255 ) ^ 67 )
215 .chr( ( $n & 255 ) ^ 67 );
216 @s = @s[ 4 .. $#s ];
218 $r = substr( $r, 0, length($r) - $l );
219 return $r;
222 =head2 transferbook
224 ($dotransfer, $messages, $iteminformation) = &transferbook($newbranch,
225 $barcode, $ignore_reserves);
227 Transfers an item to a new branch. If the item is currently on loan, it is automatically returned before the actual transfer.
229 C<$newbranch> is the code for the branch to which the item should be transferred.
231 C<$barcode> is the barcode of the item to be transferred.
233 If C<$ignore_reserves> is true, C<&transferbook> ignores reserves.
234 Otherwise, if an item is reserved, the transfer fails.
236 Returns three values:
238 =over
240 =item $dotransfer
242 is true if the transfer was successful.
244 =item $messages
246 is a reference-to-hash which may have any of the following keys:
248 =over
250 =item C<BadBarcode>
252 There is no item in the catalog with the given barcode. The value is C<$barcode>.
254 =item C<IsPermanent>
256 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.
258 =item C<DestinationEqualsHolding>
260 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.
262 =item C<WasReturned>
264 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.
266 =item C<ResFound>
268 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>.
270 =item C<WasTransferred>
272 The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored.
274 =back
276 =back
278 =cut
280 sub transferbook {
281 my ( $tbr, $barcode, $ignoreRs ) = @_;
282 my $messages;
283 my $dotransfer = 1;
284 my $branches = GetBranches();
285 my $itemnumber = GetItemnumberFromBarcode( $barcode );
286 my $issue = GetItemIssue($itemnumber);
287 my $biblio = GetBiblioFromItemNumber($itemnumber);
289 # bad barcode..
290 if ( not $itemnumber ) {
291 $messages->{'BadBarcode'} = $barcode;
292 $dotransfer = 0;
295 # get branches of book...
296 my $hbr = $biblio->{'homebranch'};
297 my $fbr = $biblio->{'holdingbranch'};
299 # if using Branch Transfer Limits
300 if ( C4::Context->preference("UseBranchTransferLimits") == 1 ) {
301 if ( C4::Context->preference("item-level_itypes") && C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ) {
302 if ( ! IsBranchTransferAllowed( $tbr, $fbr, $biblio->{'itype'} ) ) {
303 $messages->{'NotAllowed'} = $tbr . "::" . $biblio->{'itype'};
304 $dotransfer = 0;
306 } elsif ( ! IsBranchTransferAllowed( $tbr, $fbr, $biblio->{ C4::Context->preference("BranchTransferLimitsType") } ) ) {
307 $messages->{'NotAllowed'} = $tbr . "::" . $biblio->{ C4::Context->preference("BranchTransferLimitsType") };
308 $dotransfer = 0;
312 # if is permanent...
313 if ( $hbr && $branches->{$hbr}->{'PE'} ) {
314 $messages->{'IsPermanent'} = $hbr;
315 $dotransfer = 0;
318 # can't transfer book if is already there....
319 if ( $fbr eq $tbr ) {
320 $messages->{'DestinationEqualsHolding'} = 1;
321 $dotransfer = 0;
324 # check if it is still issued to someone, return it...
325 if ($issue->{borrowernumber}) {
326 AddReturn( $barcode, $fbr );
327 $messages->{'WasReturned'} = $issue->{borrowernumber};
330 # find reserves.....
331 # That'll save a database query.
332 my ( $resfound, $resrec, undef ) =
333 CheckReserves( $itemnumber );
334 if ( $resfound and not $ignoreRs ) {
335 $resrec->{'ResFound'} = $resfound;
337 # $messages->{'ResFound'} = $resrec;
338 $dotransfer = 1;
341 #actually do the transfer....
342 if ($dotransfer) {
343 ModItemTransfer( $itemnumber, $fbr, $tbr );
345 # don't need to update MARC anymore, we do it in batch now
346 $messages->{'WasTransfered'} = 1;
349 ModDateLastSeen( $itemnumber );
350 return ( $dotransfer, $messages, $biblio );
354 sub TooMany {
355 my $borrower = shift;
356 my $biblionumber = shift;
357 my $item = shift;
358 my $cat_borrower = $borrower->{'categorycode'};
359 my $dbh = C4::Context->dbh;
360 my $branch;
361 # Get which branchcode we need
362 $branch = _GetCircControlBranch($item,$borrower);
363 my $type = (C4::Context->preference('item-level_itypes'))
364 ? $item->{'itype'} # item-level
365 : $item->{'itemtype'}; # biblio-level
367 # given branch, patron category, and item type, determine
368 # applicable issuing rule
369 my $issuing_rule = GetIssuingRule($cat_borrower, $type, $branch);
371 # if a rule is found and has a loan limit set, count
372 # how many loans the patron already has that meet that
373 # rule
374 if (defined($issuing_rule) and defined($issuing_rule->{'maxissueqty'})) {
375 my @bind_params;
376 my $count_query = "SELECT COUNT(*) FROM issues
377 JOIN items USING (itemnumber) ";
379 my $rule_itemtype = $issuing_rule->{itemtype};
380 if ($rule_itemtype eq "*") {
381 # matching rule has the default item type, so count only
382 # those existing loans that don't fall under a more
383 # specific rule
384 if (C4::Context->preference('item-level_itypes')) {
385 $count_query .= " WHERE items.itype NOT IN (
386 SELECT itemtype FROM issuingrules
387 WHERE branchcode = ?
388 AND (categorycode = ? OR categorycode = ?)
389 AND itemtype <> '*'
390 ) ";
391 } else {
392 $count_query .= " JOIN biblioitems USING (biblionumber)
393 WHERE biblioitems.itemtype NOT IN (
394 SELECT itemtype FROM issuingrules
395 WHERE branchcode = ?
396 AND (categorycode = ? OR categorycode = ?)
397 AND itemtype <> '*'
398 ) ";
400 push @bind_params, $issuing_rule->{branchcode};
401 push @bind_params, $issuing_rule->{categorycode};
402 push @bind_params, $cat_borrower;
403 } else {
404 # rule has specific item type, so count loans of that
405 # specific item type
406 if (C4::Context->preference('item-level_itypes')) {
407 $count_query .= " WHERE items.itype = ? ";
408 } else {
409 $count_query .= " JOIN biblioitems USING (biblionumber)
410 WHERE biblioitems.itemtype= ? ";
412 push @bind_params, $type;
415 $count_query .= " AND borrowernumber = ? ";
416 push @bind_params, $borrower->{'borrowernumber'};
417 my $rule_branch = $issuing_rule->{branchcode};
418 if ($rule_branch ne "*") {
419 if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
420 $count_query .= " AND issues.branchcode = ? ";
421 push @bind_params, $branch;
422 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
423 ; # if branch is the patron's home branch, then count all loans by patron
424 } else {
425 $count_query .= " AND items.homebranch = ? ";
426 push @bind_params, $branch;
430 my $count_sth = $dbh->prepare($count_query);
431 $count_sth->execute(@bind_params);
432 my ($current_loan_count) = $count_sth->fetchrow_array;
434 my $max_loans_allowed = $issuing_rule->{'maxissueqty'};
435 if ($current_loan_count >= $max_loans_allowed) {
436 return ($current_loan_count, $max_loans_allowed);
440 # Now count total loans against the limit for the branch
441 my $branch_borrower_circ_rule = GetBranchBorrowerCircRule($branch, $cat_borrower);
442 if (defined($branch_borrower_circ_rule->{maxissueqty})) {
443 my @bind_params = ();
444 my $branch_count_query = "SELECT COUNT(*) FROM issues
445 JOIN items USING (itemnumber)
446 WHERE borrowernumber = ? ";
447 push @bind_params, $borrower->{borrowernumber};
449 if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
450 $branch_count_query .= " AND issues.branchcode = ? ";
451 push @bind_params, $branch;
452 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
453 ; # if branch is the patron's home branch, then count all loans by patron
454 } else {
455 $branch_count_query .= " AND items.homebranch = ? ";
456 push @bind_params, $branch;
458 my $branch_count_sth = $dbh->prepare($branch_count_query);
459 $branch_count_sth->execute(@bind_params);
460 my ($current_loan_count) = $branch_count_sth->fetchrow_array;
462 my $max_loans_allowed = $branch_borrower_circ_rule->{maxissueqty};
463 if ($current_loan_count >= $max_loans_allowed) {
464 return ($current_loan_count, $max_loans_allowed);
468 # OK, the patron can issue !!!
469 return;
472 =head2 itemissues
474 @issues = &itemissues($biblioitemnumber, $biblio);
476 Looks up information about who has borrowed the bookZ<>(s) with the
477 given biblioitemnumber.
479 C<$biblio> is ignored.
481 C<&itemissues> returns an array of references-to-hash. The keys
482 include the fields from the C<items> table in the Koha database.
483 Additional keys include:
485 =over 4
487 =item C<date_due>
489 If the item is currently on loan, this gives the due date.
491 If the item is not on loan, then this is either "Available" or
492 "Cancelled", if the item has been withdrawn.
494 =item C<card>
496 If the item is currently on loan, this gives the card number of the
497 patron who currently has the item.
499 =item C<timestamp0>, C<timestamp1>, C<timestamp2>
501 These give the timestamp for the last three times the item was
502 borrowed.
504 =item C<card0>, C<card1>, C<card2>
506 The card number of the last three patrons who borrowed this item.
508 =item C<borrower0>, C<borrower1>, C<borrower2>
510 The borrower number of the last three patrons who borrowed this item.
512 =back
514 =cut
517 sub itemissues {
518 my ( $bibitem, $biblio ) = @_;
519 my $dbh = C4::Context->dbh;
520 my $sth =
521 $dbh->prepare("Select * from items where items.biblioitemnumber = ?")
522 || die $dbh->errstr;
523 my $i = 0;
524 my @results;
526 $sth->execute($bibitem) || die $sth->errstr;
528 while ( my $data = $sth->fetchrow_hashref ) {
530 # Find out who currently has this item.
531 # FIXME - Wouldn't it be better to do this as a left join of
532 # some sort? Currently, this code assumes that if
533 # fetchrow_hashref() fails, then the book is on the shelf.
534 # fetchrow_hashref() can fail for any number of reasons (e.g.,
535 # database server crash), not just because no items match the
536 # search criteria.
537 my $sth2 = $dbh->prepare(
538 "SELECT * FROM issues
539 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
540 WHERE itemnumber = ?
544 $sth2->execute( $data->{'itemnumber'} );
545 if ( my $data2 = $sth2->fetchrow_hashref ) {
546 $data->{'date_due'} = $data2->{'date_due'};
547 $data->{'card'} = $data2->{'cardnumber'};
548 $data->{'borrower'} = $data2->{'borrowernumber'};
550 else {
551 $data->{'date_due'} = ($data->{'wthdrawn'} eq '1') ? 'Cancelled' : 'Available';
555 # Find the last 3 people who borrowed this item.
556 $sth2 = $dbh->prepare(
557 "SELECT * FROM old_issues
558 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
559 WHERE itemnumber = ?
560 ORDER BY returndate DESC,timestamp DESC"
563 $sth2->execute( $data->{'itemnumber'} );
564 for ( my $i2 = 0 ; $i2 < 2 ; $i2++ )
565 { # FIXME : error if there is less than 3 pple borrowing this item
566 if ( my $data2 = $sth2->fetchrow_hashref ) {
567 $data->{"timestamp$i2"} = $data2->{'timestamp'};
568 $data->{"card$i2"} = $data2->{'cardnumber'};
569 $data->{"borrower$i2"} = $data2->{'borrowernumber'};
570 } # if
571 } # for
573 $results[$i] = $data;
574 $i++;
577 return (@results);
580 =head2 CanBookBeIssued
582 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $borrower,
583 $barcode, $duedatespec, $inprocess, $ignore_reserves );
585 Check if a book can be issued.
587 C<$issuingimpossible> and C<$needsconfirmation> are some hashref.
589 =over 4
591 =item C<$borrower> hash with borrower informations (from GetMember or GetMemberDetails)
593 =item C<$barcode> is the bar code of the book being issued.
595 =item C<$duedatespec> is a C4::Dates object.
597 =item C<$inprocess> boolean switch
598 =item C<$ignore_reserves> boolean switch
600 =back
602 Returns :
604 =over 4
606 =item C<$issuingimpossible> a reference to a hash. It contains reasons why issuing is impossible.
607 Possible values are :
609 =back
611 =head3 INVALID_DATE
613 sticky due date is invalid
615 =head3 GNA
617 borrower gone with no address
619 =head3 CARD_LOST
621 borrower declared it's card lost
623 =head3 DEBARRED
625 borrower debarred
627 =head3 UNKNOWN_BARCODE
629 barcode unknown
631 =head3 NOT_FOR_LOAN
633 item is not for loan
635 =head3 WTHDRAWN
637 item withdrawn.
639 =head3 RESTRICTED
641 item is restricted (set by ??)
643 C<$needsconfirmation> a reference to a hash. It contains reasons why the loan
644 could be prevented, but ones that can be overriden by the operator.
646 Possible values are :
648 =head3 DEBT
650 borrower has debts.
652 =head3 RENEW_ISSUE
654 renewing, not issuing
656 =head3 ISSUED_TO_ANOTHER
658 issued to someone else.
660 =head3 RESERVED
662 reserved for someone else.
664 =head3 INVALID_DATE
666 sticky due date is invalid or due date in the past
668 =head3 TOO_MANY
670 if the borrower borrows to much things
672 =cut
674 sub CanBookBeIssued {
675 my ( $borrower, $barcode, $duedate, $inprocess, $ignore_reserves ) = @_;
676 my %needsconfirmation; # filled with problems that needs confirmations
677 my %issuingimpossible; # filled with problems that causes the issue to be IMPOSSIBLE
678 my $item = GetItem(GetItemnumberFromBarcode( $barcode ));
679 my $issue = GetItemIssue($item->{itemnumber});
680 my $biblioitem = GetBiblioItemData($item->{biblioitemnumber});
681 $item->{'itemtype'}=$item->{'itype'};
682 my $dbh = C4::Context->dbh;
684 # MANDATORY CHECKS - unless item exists, nothing else matters
685 unless ( $item->{barcode} ) {
686 $issuingimpossible{UNKNOWN_BARCODE} = 1;
688 return ( \%issuingimpossible, \%needsconfirmation ) if %issuingimpossible;
691 # DUE DATE is OK ? -- should already have checked.
693 unless ( $duedate ) {
694 my $issuedate = strftime( "%Y-%m-%d", localtime );
696 my $branch = _GetCircControlBranch($item,$borrower);
697 my $itype = ( C4::Context->preference('item-level_itypes') ) ? $item->{'itype'} : $biblioitem->{'itemtype'};
698 $duedate = CalcDateDue( C4::Dates->new( $issuedate, 'iso' ), $itype, $branch, $borrower );
700 # Offline circ calls AddIssue directly, doesn't run through here
701 # So issuingimpossible should be ok.
703 if ($duedate) {
704 $needsconfirmation{INVALID_DATE} = $duedate->output('syspref')
705 unless $duedate->output('iso') ge C4::Dates->today('iso');
706 } else {
707 $issuingimpossible{INVALID_DATE} = $duedate->output('syspref');
711 # BORROWER STATUS
713 if ( $borrower->{'category_type'} eq 'X' && ( $item->{barcode} )) {
714 # stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1 .
715 &UpdateStats(C4::Context->userenv->{'branch'},'localuse','','',$item->{'itemnumber'},$item->{'itemtype'},$borrower->{'borrowernumber'});
716 ModDateLastSeen( $item->{'itemnumber'} );
717 return( { STATS => 1 }, {});
719 if ( $borrower->{flags}->{GNA} ) {
720 $issuingimpossible{GNA} = 1;
722 if ( $borrower->{flags}->{'LOST'} ) {
723 $issuingimpossible{CARD_LOST} = 1;
725 if ( $borrower->{flags}->{'DBARRED'} ) {
726 $issuingimpossible{DEBARRED} = 1;
728 if ( $borrower->{'dateexpiry'} eq '0000-00-00') {
729 $issuingimpossible{EXPIRED} = 1;
730 } else {
731 my @expirydate= split /-/,$borrower->{'dateexpiry'};
732 if($expirydate[0]==0 || $expirydate[1]==0|| $expirydate[2]==0 ||
733 Date_to_Days(Today) > Date_to_Days( @expirydate )) {
734 $issuingimpossible{EXPIRED} = 1;
738 # BORROWER STATUS
741 # DEBTS
742 my ($amount) =
743 C4::Members::GetMemberAccountRecords( $borrower->{'borrowernumber'}, '' && $duedate->output('iso') );
744 my $amountlimit = C4::Context->preference("noissuescharge");
745 my $allowfineoverride = C4::Context->preference("AllowFineOverride");
746 my $allfinesneedoverride = C4::Context->preference("AllFinesNeedOverride");
747 if ( C4::Context->preference("IssuingInProcess") ) {
748 if ( $amount > $amountlimit && !$inprocess && !$allowfineoverride) {
749 $issuingimpossible{DEBT} = sprintf( "%.2f", $amount );
750 } elsif ( $amount > $amountlimit && !$inprocess && $allowfineoverride) {
751 $needsconfirmation{DEBT} = sprintf( "%.2f", $amount );
752 } elsif ( $allfinesneedoverride && $amount > 0 && $amount <= $amountlimit && !$inprocess ) {
753 $needsconfirmation{DEBT} = sprintf( "%.2f", $amount );
756 else {
757 if ( $amount > $amountlimit && $allowfineoverride ) {
758 $needsconfirmation{DEBT} = sprintf( "%.2f", $amount );
759 } elsif ( $amount > $amountlimit && !$allowfineoverride) {
760 $issuingimpossible{DEBT} = sprintf( "%.2f", $amount );
761 } elsif ( $amount > 0 && $allfinesneedoverride ) {
762 $needsconfirmation{DEBT} = sprintf( "%.2f", $amount );
766 my ($blocktype, $count) = C4::Members::IsMemberBlocked($borrower->{'borrowernumber'});
767 if ($blocktype == -1) {
768 ## patron has outstanding overdue loans
769 if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){
770 $issuingimpossible{USERBLOCKEDOVERDUE} = $count;
772 elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){
773 $needsconfirmation{USERBLOCKEDOVERDUE} = $count;
775 } elsif($blocktype == 1) {
776 # patron has accrued fine days
777 $issuingimpossible{USERBLOCKEDREMAINING} = $count;
781 # JB34 CHECKS IF BORROWERS DONT HAVE ISSUE TOO MANY BOOKS
783 my ($current_loan_count, $max_loans_allowed) = TooMany( $borrower, $item->{biblionumber}, $item );
784 # if TooMany max_loans_allowed returns 0 the user doesn't have permission to check out this book
785 if ($max_loans_allowed eq 0) {
786 $needsconfirmation{PATRON_CANT} = 1;
787 } else {
788 if($max_loans_allowed){
789 $needsconfirmation{TOO_MANY} = 1;
790 $needsconfirmation{current_loan_count} = $current_loan_count;
791 $needsconfirmation{max_loans_allowed} = $max_loans_allowed;
796 # ITEM CHECKING
798 if ( $item->{'notforloan'}
799 && $item->{'notforloan'} > 0 )
801 if(!C4::Context->preference("AllowNotForLoanOverride")){
802 $issuingimpossible{NOT_FOR_LOAN} = 1;
803 }else{
804 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
807 elsif ( !$item->{'notforloan'} ){
808 # we have to check itemtypes.notforloan also
809 if (C4::Context->preference('item-level_itypes')){
810 # this should probably be a subroutine
811 my $sth = $dbh->prepare("SELECT notforloan FROM itemtypes WHERE itemtype = ?");
812 $sth->execute($item->{'itemtype'});
813 my $notforloan=$sth->fetchrow_hashref();
814 $sth->finish();
815 if ($notforloan->{'notforloan'}) {
816 if (!C4::Context->preference("AllowNotForLoanOverride")) {
817 $issuingimpossible{NOT_FOR_LOAN} = 1;
818 } else {
819 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
823 elsif ($biblioitem->{'notforloan'} == 1){
824 if (!C4::Context->preference("AllowNotForLoanOverride")) {
825 $issuingimpossible{NOT_FOR_LOAN} = 1;
826 } else {
827 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
831 if ( $item->{'wthdrawn'} && $item->{'wthdrawn'} > 0 )
833 $issuingimpossible{WTHDRAWN} = 1;
835 if ( $item->{'restricted'}
836 && $item->{'restricted'} == 1 )
838 $issuingimpossible{RESTRICTED} = 1;
840 if ( C4::Context->preference("IndependantBranches") ) {
841 my $userenv = C4::Context->userenv;
842 if ( ($userenv) && ( $userenv->{flags} % 2 != 1 ) ) {
843 $issuingimpossible{ITEMNOTSAMEBRANCH} = 1
844 if ( $item->{C4::Context->preference("HomeOrHoldingBranch")} ne $userenv->{branch} );
845 $needsconfirmation{BORRNOTSAMEBRANCH} = GetBranchName( $borrower->{'branchcode'} )
846 if ( $borrower->{'branchcode'} ne $userenv->{branch} );
851 # CHECK IF BOOK ALREADY ISSUED TO THIS BORROWER
853 if ( $issue->{borrowernumber} && $issue->{borrowernumber} eq $borrower->{'borrowernumber'} )
856 # Already issued to current borrower. Ask whether the loan should
857 # be renewed.
858 my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed(
859 $borrower->{'borrowernumber'},
860 $item->{'itemnumber'}
862 if ( $CanBookBeRenewed == 0 ) { # no more renewals allowed
863 $issuingimpossible{NO_MORE_RENEWALS} = 1;
865 else {
866 $needsconfirmation{RENEW_ISSUE} = 1;
869 elsif ($issue->{borrowernumber}) {
871 # issued to someone else
872 my $currborinfo = C4::Members::GetMember( borrowernumber => $issue->{borrowernumber} );
874 # warn "=>.$currborinfo->{'firstname'} $currborinfo->{'surname'} ($currborinfo->{'cardnumber'})";
875 $needsconfirmation{ISSUED_TO_ANOTHER} = 1;
876 $needsconfirmation{issued_firstname} = $currborinfo->{'firstname'};
877 $needsconfirmation{issued_surname} = $currborinfo->{'surname'};
878 $needsconfirmation{issued_cardnumber} = $currborinfo->{'cardnumber'};
879 $needsconfirmation{issued_borrowernumber} = $currborinfo->{'borrowernumber'};
882 unless ( $ignore_reserves ) {
883 # See if the item is on reserve.
884 my ( $restype, $res ) = C4::Reserves::CheckReserves( $item->{'itemnumber'} );
885 if ($restype) {
886 my $resbor = $res->{'borrowernumber'};
887 if ( $resbor ne $borrower->{'borrowernumber'} ) {
888 my ( $resborrower ) = C4::Members::GetMember( borrowernumber => $resbor );
889 my $branchname = GetBranchName( $res->{'branchcode'} );
890 if ( $restype eq "Waiting" )
892 # The item is on reserve and waiting, but has been
893 # reserved by some other patron.
894 $needsconfirmation{RESERVE_WAITING} = 1;
895 $needsconfirmation{'resfirstname'} = $resborrower->{'firstname'};
896 $needsconfirmation{'ressurname'} = $resborrower->{'surname'};
897 $needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'};
898 $needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'};
899 $needsconfirmation{'resbranchname'} = $branchname;
900 $needsconfirmation{'reswaitingdate'} = format_date($res->{'waitingdate'});
902 elsif ( $restype eq "Reserved" ) {
903 # The item is on reserve for someone else.
904 $needsconfirmation{RESERVED} = 1;
905 $needsconfirmation{'resfirstname'} = $resborrower->{'firstname'};
906 $needsconfirmation{'ressurname'} = $resborrower->{'surname'};
907 $needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'};
908 $needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'};
909 $needsconfirmation{'resbranchname'} = $branchname;
910 $needsconfirmation{'resreservedate'} = format_date($res->{'reservedate'});
915 return ( \%issuingimpossible, \%needsconfirmation );
918 =head2 AddIssue
920 &AddIssue($borrower, $barcode, [$datedue], [$cancelreserve], [$issuedate])
922 Issue a book. Does no check, they are done in CanBookBeIssued. If we reach this sub, it means the user confirmed if needed.
924 =over 4
926 =item C<$borrower> is a hash with borrower informations (from GetMember or GetMemberDetails).
928 =item C<$barcode> is the barcode of the item being issued.
930 =item C<$datedue> is a C4::Dates object for the max date of return, i.e. the date due (optional).
931 Calculated if empty.
933 =item C<$cancelreserve> is 1 to override and cancel any pending reserves for the item (optional).
935 =item C<$issuedate> is the date to issue the item in iso (YYYY-MM-DD) format (optional).
936 Defaults to today. Unlike C<$datedue>, NOT a C4::Dates object, unfortunately.
938 AddIssue does the following things :
940 - step 01: check that there is a borrowernumber & a barcode provided
941 - check for RENEWAL (book issued & being issued to the same patron)
942 - renewal YES = Calculate Charge & renew
943 - renewal NO =
944 * BOOK ACTUALLY ISSUED ? do a return if book is actually issued (but to someone else)
945 * RESERVE PLACED ?
946 - fill reserve if reserve to this patron
947 - cancel reserve or not, otherwise
948 * TRANSFERT PENDING ?
949 - complete the transfert
950 * ISSUE THE BOOK
952 =back
954 =cut
956 sub AddIssue {
957 my ( $borrower, $barcode, $datedue, $cancelreserve, $issuedate, $sipmode) = @_;
958 my $dbh = C4::Context->dbh;
959 my $barcodecheck=CheckValidBarcode($barcode);
960 # $issuedate defaults to today.
961 if ( ! defined $issuedate ) {
962 $issuedate = strftime( "%Y-%m-%d", localtime );
963 # TODO: for hourly circ, this will need to be a C4::Dates object
964 # and all calls to AddIssue including issuedate will need to pass a Dates object.
966 if ($borrower and $barcode and $barcodecheck ne '0'){
967 # find which item we issue
968 my $item = GetItem('', $barcode) or return undef; # if we don't get an Item, abort.
969 my $branch = _GetCircControlBranch($item,$borrower);
971 # get actual issuing if there is one
972 my $actualissue = GetItemIssue( $item->{itemnumber});
974 # get biblioinformation for this item
975 my $biblio = GetBiblioFromItemNumber($item->{itemnumber});
978 # check if we just renew the issue.
980 if ($actualissue->{borrowernumber} eq $borrower->{'borrowernumber'}) {
981 $datedue = AddRenewal(
982 $borrower->{'borrowernumber'},
983 $item->{'itemnumber'},
984 $branch,
985 $datedue,
986 $issuedate, # here interpreted as the renewal date
989 else {
990 # it's NOT a renewal
991 if ( $actualissue->{borrowernumber}) {
992 # This book is currently on loan, but not to the person
993 # who wants to borrow it now. mark it returned before issuing to the new borrower
994 AddReturn(
995 $item->{'barcode'},
996 C4::Context->userenv->{'branch'}
1000 MoveReserve( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $cancelreserve );
1002 # Starting process for transfer job (checking transfert and validate it if we have one)
1003 my ($datesent) = GetTransfers($item->{'itemnumber'});
1004 if ($datesent) {
1005 # updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....)
1006 my $sth =
1007 $dbh->prepare(
1008 "UPDATE branchtransfers
1009 SET datearrived = now(),
1010 tobranch = ?,
1011 comments = 'Forced branchtransfer'
1012 WHERE itemnumber= ? AND datearrived IS NULL"
1014 $sth->execute(C4::Context->userenv->{'branch'},$item->{'itemnumber'});
1017 # Record in the database the fact that the book was issued.
1018 my $sth =
1019 $dbh->prepare(
1020 "INSERT INTO issues
1021 (borrowernumber, itemnumber,issuedate, date_due, branchcode)
1022 VALUES (?,?,?,?,?)"
1024 unless ($datedue) {
1025 my $itype = ( C4::Context->preference('item-level_itypes') ) ? $biblio->{'itype'} : $biblio->{'itemtype'};
1026 $datedue = CalcDateDue( C4::Dates->new( $issuedate, 'iso' ), $itype, $branch, $borrower );
1029 $sth->execute(
1030 $borrower->{'borrowernumber'}, # borrowernumber
1031 $item->{'itemnumber'}, # itemnumber
1032 $issuedate, # issuedate
1033 $datedue->output('iso'), # date_due
1034 C4::Context->userenv->{'branch'} # branchcode
1036 $sth->finish;
1037 if ( C4::Context->preference('ReturnToShelvingCart') ) { ## ReturnToShelvingCart is on, anything issued should be taken off the cart.
1038 CartToShelf( $item->{'itemnumber'} );
1040 $item->{'issues'}++;
1042 ## If item was lost, it has now been found, reverse any list item charges if neccessary.
1043 if ( $item->{'itemlost'} ) {
1044 _FixAccountForLostAndReturned( $item->{'itemnumber'}, undef, $item->{'barcode'} );
1047 ModItem({ issues => $item->{'issues'},
1048 holdingbranch => C4::Context->userenv->{'branch'},
1049 itemlost => 0,
1050 datelastborrowed => C4::Dates->new()->output('iso'),
1051 onloan => $datedue->output('iso'),
1052 }, $item->{'biblionumber'}, $item->{'itemnumber'});
1053 ModDateLastSeen( $item->{'itemnumber'} );
1055 # If it costs to borrow this book, charge it to the patron's account.
1056 my ( $charge, $itemtype ) = GetIssuingCharges(
1057 $item->{'itemnumber'},
1058 $borrower->{'borrowernumber'}
1060 if ( $charge > 0 ) {
1061 AddIssuingCharge(
1062 $item->{'itemnumber'},
1063 $borrower->{'borrowernumber'}, $charge
1065 $item->{'charge'} = $charge;
1068 # Record the fact that this book was issued.
1069 &UpdateStats(
1070 C4::Context->userenv->{'branch'},
1071 'issue', $charge,
1072 ($sipmode ? "SIP-$sipmode" : ''), $item->{'itemnumber'},
1073 $item->{'itype'}, $borrower->{'borrowernumber'}
1076 # Send a checkout slip.
1077 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1078 my %conditions = (
1079 branchcode => $branch,
1080 categorycode => $borrower->{categorycode},
1081 item_type => $item->{itype},
1082 notification => 'CHECKOUT',
1084 if ($circulation_alert->is_enabled_for(\%conditions)) {
1085 SendCirculationAlert({
1086 type => 'CHECKOUT',
1087 item => $item,
1088 borrower => $borrower,
1089 branch => $branch,
1094 logaction("CIRCULATION", "ISSUE", $borrower->{'borrowernumber'}, $biblio->{'biblionumber'})
1095 if C4::Context->preference("IssueLog");
1097 return ($datedue); # not necessarily the same as when it came in!
1100 =head2 GetLoanLength
1102 my $loanlength = &GetLoanLength($borrowertype,$itemtype,branchcode)
1104 Get loan length for an itemtype, a borrower type and a branch
1106 =cut
1108 sub GetLoanLength {
1109 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1110 my $dbh = C4::Context->dbh;
1111 my $sth =
1112 $dbh->prepare(
1113 "select issuelength from issuingrules where categorycode=? and itemtype=? and branchcode=? and issuelength is not null"
1115 # warn "in get loan lenght $borrowertype $itemtype $branchcode ";
1116 # try to find issuelength & return the 1st available.
1117 # check with borrowertype, itemtype and branchcode, then without one of those parameters
1118 $sth->execute( $borrowertype, $itemtype, $branchcode );
1119 my $loanlength = $sth->fetchrow_hashref;
1120 return $loanlength->{issuelength}
1121 if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
1123 $sth->execute( $borrowertype, "*", $branchcode );
1124 $loanlength = $sth->fetchrow_hashref;
1125 return $loanlength->{issuelength}
1126 if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
1128 $sth->execute( "*", $itemtype, $branchcode );
1129 $loanlength = $sth->fetchrow_hashref;
1130 return $loanlength->{issuelength}
1131 if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
1133 $sth->execute( "*", "*", $branchcode );
1134 $loanlength = $sth->fetchrow_hashref;
1135 return $loanlength->{issuelength}
1136 if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
1138 $sth->execute( $borrowertype, $itemtype, "*" );
1139 $loanlength = $sth->fetchrow_hashref;
1140 return $loanlength->{issuelength}
1141 if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
1143 $sth->execute( $borrowertype, "*", "*" );
1144 $loanlength = $sth->fetchrow_hashref;
1145 return $loanlength->{issuelength}
1146 if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
1148 $sth->execute( "*", $itemtype, "*" );
1149 $loanlength = $sth->fetchrow_hashref;
1150 return $loanlength->{issuelength}
1151 if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
1153 $sth->execute( "*", "*", "*" );
1154 $loanlength = $sth->fetchrow_hashref;
1155 return $loanlength->{issuelength}
1156 if defined($loanlength) && $loanlength->{issuelength} ne 'NULL';
1158 # if no rule is set => 21 days (hardcoded)
1159 return 21;
1163 =head2 GetHardDueDate
1165 my ($hardduedate,$hardduedatecompare) = &GetHardDueDate($borrowertype,$itemtype,branchcode)
1167 Get the Hard Due Date and it's comparison for an itemtype, a borrower type and a branch
1169 =cut
1171 sub GetHardDueDate {
1172 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1173 my $dbh = C4::Context->dbh;
1174 my $sth =
1175 $dbh->prepare(
1176 "select hardduedate, hardduedatecompare from issuingrules where categorycode=? and itemtype=? and branchcode=?"
1178 $sth->execute( $borrowertype, $itemtype, $branchcode );
1179 my $results = $sth->fetchrow_hashref;
1180 return (C4::Dates->new($results->{hardduedate}, 'iso'),$results->{hardduedatecompare})
1181 if defined($results) && $results->{hardduedate} ne 'NULL';
1183 $sth->execute( $borrowertype, "*", $branchcode );
1184 $results = $sth->fetchrow_hashref;
1185 return (C4::Dates->new($results->{hardduedate}, 'iso'),$results->{hardduedatecompare})
1186 if defined($results) && $results->{hardduedate} ne 'NULL';
1188 $sth->execute( "*", $itemtype, $branchcode );
1189 $results = $sth->fetchrow_hashref;
1190 return (C4::Dates->new($results->{hardduedate}, 'iso'),$results->{hardduedatecompare})
1191 if defined($results) && $results->{hardduedate} ne 'NULL';
1193 $sth->execute( "*", "*", $branchcode );
1194 $results = $sth->fetchrow_hashref;
1195 return (C4::Dates->new($results->{hardduedate}, 'iso'),$results->{hardduedatecompare})
1196 if defined($results) && $results->{hardduedate} ne 'NULL';
1198 $sth->execute( $borrowertype, $itemtype, "*" );
1199 $results = $sth->fetchrow_hashref;
1200 return (C4::Dates->new($results->{hardduedate}, 'iso'),$results->{hardduedatecompare})
1201 if defined($results) && $results->{hardduedate} ne 'NULL';
1203 $sth->execute( $borrowertype, "*", "*" );
1204 $results = $sth->fetchrow_hashref;
1205 return (C4::Dates->new($results->{hardduedate}, 'iso'),$results->{hardduedatecompare})
1206 if defined($results) && $results->{hardduedate} ne 'NULL';
1208 $sth->execute( "*", $itemtype, "*" );
1209 $results = $sth->fetchrow_hashref;
1210 return (C4::Dates->new($results->{hardduedate}, 'iso'),$results->{hardduedatecompare})
1211 if defined($results) && $results->{hardduedate} ne 'NULL';
1213 $sth->execute( "*", "*", "*" );
1214 $results = $sth->fetchrow_hashref;
1215 return (C4::Dates->new($results->{hardduedate}, 'iso'),$results->{hardduedatecompare})
1216 if defined($results) && $results->{hardduedate} ne 'NULL';
1218 # if no rule is set => return undefined
1219 return (undef, undef);
1222 =head2 GetIssuingRule
1224 my $irule = &GetIssuingRule($borrowertype,$itemtype,branchcode)
1226 FIXME - This is a copy-paste of GetLoanLength
1227 as a stop-gap. Do not wish to change API for GetLoanLength
1228 this close to release, however, Overdues::GetIssuingRules is broken.
1230 Get the issuing rule for an itemtype, a borrower type and a branch
1231 Returns a hashref from the issuingrules table.
1233 =cut
1235 sub GetIssuingRule {
1236 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1237 my $dbh = C4::Context->dbh;
1238 my $sth = $dbh->prepare( "select * from issuingrules where categorycode=? and itemtype=? and branchcode=? and issuelength is not null" );
1239 my $irule;
1241 $sth->execute( $borrowertype, $itemtype, $branchcode );
1242 $irule = $sth->fetchrow_hashref;
1243 return $irule if defined($irule) ;
1245 $sth->execute( $borrowertype, "*", $branchcode );
1246 $irule = $sth->fetchrow_hashref;
1247 return $irule if defined($irule) ;
1249 $sth->execute( "*", $itemtype, $branchcode );
1250 $irule = $sth->fetchrow_hashref;
1251 return $irule if defined($irule) ;
1253 $sth->execute( "*", "*", $branchcode );
1254 $irule = $sth->fetchrow_hashref;
1255 return $irule if defined($irule) ;
1257 $sth->execute( $borrowertype, $itemtype, "*" );
1258 $irule = $sth->fetchrow_hashref;
1259 return $irule if defined($irule) ;
1261 $sth->execute( $borrowertype, "*", "*" );
1262 $irule = $sth->fetchrow_hashref;
1263 return $irule if defined($irule) ;
1265 $sth->execute( "*", $itemtype, "*" );
1266 $irule = $sth->fetchrow_hashref;
1267 return $irule if defined($irule) ;
1269 $sth->execute( "*", "*", "*" );
1270 $irule = $sth->fetchrow_hashref;
1271 return $irule if defined($irule) ;
1273 # if no rule matches,
1274 return undef;
1277 =head2 GetBranchBorrowerCircRule
1279 my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
1281 Retrieves circulation rule attributes that apply to the given
1282 branch and patron category, regardless of item type.
1283 The return value is a hashref containing the following key:
1285 maxissueqty - maximum number of loans that a
1286 patron of the given category can have at the given
1287 branch. If the value is undef, no limit.
1289 This will first check for a specific branch and
1290 category match from branch_borrower_circ_rules.
1292 If no rule is found, it will then check default_branch_circ_rules
1293 (same branch, default category). If no rule is found,
1294 it will then check default_borrower_circ_rules (default
1295 branch, same category), then failing that, default_circ_rules
1296 (default branch, default category).
1298 If no rule has been found in the database, it will default to
1299 the buillt in rule:
1301 maxissueqty - undef
1303 C<$branchcode> and C<$categorycode> should contain the
1304 literal branch code and patron category code, respectively - no
1305 wildcards.
1307 =cut
1309 sub GetBranchBorrowerCircRule {
1310 my $branchcode = shift;
1311 my $categorycode = shift;
1313 my $branch_cat_query = "SELECT maxissueqty
1314 FROM branch_borrower_circ_rules
1315 WHERE branchcode = ?
1316 AND categorycode = ?";
1317 my $dbh = C4::Context->dbh();
1318 my $sth = $dbh->prepare($branch_cat_query);
1319 $sth->execute($branchcode, $categorycode);
1320 my $result;
1321 if ($result = $sth->fetchrow_hashref()) {
1322 return $result;
1325 # try same branch, default borrower category
1326 my $branch_query = "SELECT maxissueqty
1327 FROM default_branch_circ_rules
1328 WHERE branchcode = ?";
1329 $sth = $dbh->prepare($branch_query);
1330 $sth->execute($branchcode);
1331 if ($result = $sth->fetchrow_hashref()) {
1332 return $result;
1335 # try default branch, same borrower category
1336 my $category_query = "SELECT maxissueqty
1337 FROM default_borrower_circ_rules
1338 WHERE categorycode = ?";
1339 $sth = $dbh->prepare($category_query);
1340 $sth->execute($categorycode);
1341 if ($result = $sth->fetchrow_hashref()) {
1342 return $result;
1345 # try default branch, default borrower category
1346 my $default_query = "SELECT maxissueqty
1347 FROM default_circ_rules";
1348 $sth = $dbh->prepare($default_query);
1349 $sth->execute();
1350 if ($result = $sth->fetchrow_hashref()) {
1351 return $result;
1354 # built-in default circulation rule
1355 return {
1356 maxissueqty => undef,
1360 =head2 GetBranchItemRule
1362 my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
1364 Retrieves circulation rule attributes that apply to the given
1365 branch and item type, regardless of patron category.
1367 The return value is a hashref containing the following key:
1369 holdallowed => Hold policy for this branch and itemtype. Possible values:
1370 0: No holds allowed.
1371 1: Holds allowed only by patrons that have the same homebranch as the item.
1372 2: Holds allowed from any patron.
1374 This searches branchitemrules in the following order:
1376 * Same branchcode and itemtype
1377 * Same branchcode, itemtype '*'
1378 * branchcode '*', same itemtype
1379 * branchcode and itemtype '*'
1381 Neither C<$branchcode> nor C<$categorycode> should be '*'.
1383 =cut
1385 sub GetBranchItemRule {
1386 my ( $branchcode, $itemtype ) = @_;
1387 my $dbh = C4::Context->dbh();
1388 my $result = {};
1390 my @attempts = (
1391 ['SELECT holdallowed
1392 FROM branch_item_rules
1393 WHERE branchcode = ?
1394 AND itemtype = ?', $branchcode, $itemtype],
1395 ['SELECT holdallowed
1396 FROM default_branch_circ_rules
1397 WHERE branchcode = ?', $branchcode],
1398 ['SELECT holdallowed
1399 FROM default_branch_item_rules
1400 WHERE itemtype = ?', $itemtype],
1401 ['SELECT holdallowed
1402 FROM default_circ_rules'],
1405 foreach my $attempt (@attempts) {
1406 my ($query, @bind_params) = @{$attempt};
1408 # Since branch/category and branch/itemtype use the same per-branch
1409 # defaults tables, we have to check that the key we want is set, not
1410 # just that a row was returned
1411 return $result if ( defined( $result->{'holdallowed'} = $dbh->selectrow_array( $query, {}, @bind_params ) ) );
1414 # built-in default circulation rule
1415 return {
1416 holdallowed => 2,
1420 =head2 AddReturn
1422 ($doreturn, $messages, $iteminformation, $borrower) =
1423 &AddReturn($barcode, $branch, $exemptfine, $dropbox);
1425 Returns a book.
1427 =over 4
1429 =item C<$barcode> is the bar code of the book being returned.
1431 =item C<$branch> is the code of the branch where the book is being returned.
1433 =item C<$exemptfine> indicates that overdue charges for the item will be
1434 removed.
1436 =item C<$dropbox> indicates that the check-in date is assumed to be
1437 yesterday, or the last non-holiday as defined in C4::Calendar . If
1438 overdue charges are applied and C<$dropbox> is true, the last charge
1439 will be removed. This assumes that the fines accrual script has run
1440 for _today_.
1442 =back
1444 C<&AddReturn> returns a list of four items:
1446 C<$doreturn> is true iff the return succeeded.
1448 C<$messages> is a reference-to-hash giving feedback on the operation.
1449 The keys of the hash are:
1451 =over 4
1453 =item C<BadBarcode>
1455 No item with this barcode exists. The value is C<$barcode>.
1457 =item C<NotIssued>
1459 The book is not currently on loan. The value is C<$barcode>.
1461 =item C<IsPermanent>
1463 The book's home branch is a permanent collection. If you have borrowed
1464 this book, you are not allowed to return it. The value is the code for
1465 the book's home branch.
1467 =item C<wthdrawn>
1469 This book has been withdrawn/cancelled. The value should be ignored.
1471 =item C<Wrongbranch>
1473 This book has was returned to the wrong branch. The value is a hashref
1474 so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
1475 contain the branchcode of the incorrect and correct return library, respectively.
1477 =item C<ResFound>
1479 The item was reserved. The value is a reference-to-hash whose keys are
1480 fields from the reserves table of the Koha database, and
1481 C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
1482 either C<Waiting>, C<Reserved>, or 0.
1484 =back
1486 C<$iteminformation> is a reference-to-hash, giving information about the
1487 returned item from the issues table.
1489 C<$borrower> is a reference-to-hash, giving information about the
1490 patron who last borrowed the book.
1492 =cut
1494 sub AddReturn {
1495 my ( $barcode, $branch, $exemptfine, $dropbox ) = @_;
1496 if ($branch and not GetBranchDetail($branch)) {
1497 warn "AddReturn error: branch '$branch' not found. Reverting to " . C4::Context->userenv->{'branch'};
1498 undef $branch;
1500 $branch = C4::Context->userenv->{'branch'} unless $branch; # we trust userenv to be a safe fallback/default
1501 my $messages;
1502 my $borrower;
1503 my $biblio;
1504 my $doreturn = 1;
1505 my $validTransfert = 0;
1506 my $stat_type = 'return';
1508 # get information on item
1509 my $itemnumber = GetItemnumberFromBarcode( $barcode );
1510 unless ($itemnumber) {
1511 return (0, { BadBarcode => $barcode }); # no barcode means no item or borrower. bail out.
1513 my $issue = GetItemIssue($itemnumber);
1514 # warn Dumper($iteminformation);
1515 if ($issue and $issue->{borrowernumber}) {
1516 $borrower = C4::Members::GetMemberDetails($issue->{borrowernumber})
1517 or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existant borrowernumber '$issue->{borrowernumber}'\n"
1518 . Dumper($issue) . "\n";
1519 } else {
1520 $messages->{'NotIssued'} = $barcode;
1521 # even though item is not on loan, it may still be transferred; therefore, get current branch info
1522 $doreturn = 0;
1523 # No issue, no borrowernumber. ONLY if $doreturn, *might* you have a $borrower later.
1524 # Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
1525 if (C4::Context->preference("RecordLocalUseOnReturn")) {
1526 $messages->{'LocalUse'} = 1;
1527 $stat_type = 'localuse';
1531 my $item = GetItem($itemnumber) or die "GetItem($itemnumber) failed";
1532 # full item data, but no borrowernumber or checkout info (no issue)
1533 # we know GetItem should work because GetItemnumberFromBarcode worked
1534 my $hbr = C4::Context->preference("HomeOrHoldingBranchReturn") || "homebranch";
1535 $hbr = $item->{$hbr} || '';
1536 # item must be from items table -- issues table has branchcode and issuingbranch, not homebranch nor holdingbranch
1538 my $borrowernumber = $borrower->{'borrowernumber'} || undef; # we don't know if we had a borrower or not
1540 # check if the book is in a permanent collection....
1541 # FIXME -- This 'PE' attribute is largely undocumented. afaict, there's no user interface that reflects this functionality.
1542 if ( $hbr ) {
1543 my $branches = GetBranches(); # a potentially expensive call for a non-feature.
1544 $branches->{$hbr}->{PE} and $messages->{'IsPermanent'} = $hbr;
1547 # if indy branches and returning to different branch, refuse the return
1548 if ($hbr ne $branch && C4::Context->preference("IndependantBranches")){
1549 $messages->{'Wrongbranch'} = {
1550 Wrongbranch => $branch,
1551 Rightbranch => $hbr,
1553 $doreturn = 0;
1554 # bailing out here - in this case, current desired behavior
1555 # is to act as if no return ever happened at all.
1556 # FIXME - even in an indy branches situation, there should
1557 # still be an option for the library to accept the item
1558 # and transfer it to its owning library.
1559 return ( $doreturn, $messages, $issue, $borrower );
1562 if ( $item->{'wthdrawn'} ) { # book has been cancelled
1563 $messages->{'wthdrawn'} = 1;
1564 $doreturn = 0;
1567 # case of a return of document (deal with issues and holdingbranch)
1568 if ($doreturn) {
1569 $borrower or warn "AddReturn without current borrower";
1570 my $circControlBranch;
1571 if ($dropbox) {
1572 # define circControlBranch only if dropbox mode is set
1573 # don't allow dropbox mode to create an invalid entry in issues (issuedate > today)
1574 # FIXME: check issuedate > returndate, factoring in holidays
1575 $circControlBranch = _GetCircControlBranch($item,$borrower) unless ( $item->{'issuedate'} eq C4::Dates->today('iso') );;
1578 if ($borrowernumber) {
1579 MarkIssueReturned($borrowernumber, $item->{'itemnumber'}, $circControlBranch, '', $borrower->{'privacy'});
1580 $messages->{'WasReturned'} = 1; # FIXME is the "= 1" right? This could be the borrower hash.
1583 ModItem({ onloan => undef }, $issue->{'biblionumber'}, $item->{'itemnumber'});
1586 # the holdingbranch is updated if the document is returned to another location.
1587 # this is always done regardless of whether the item was on loan or not
1588 if ($item->{'holdingbranch'} ne $branch) {
1589 UpdateHoldingbranch($branch, $item->{'itemnumber'});
1590 $item->{'holdingbranch'} = $branch; # update item data holdingbranch too
1592 ModDateLastSeen( $item->{'itemnumber'} );
1594 # check if we have a transfer for this document
1595 my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->{'itemnumber'} );
1597 # if we have a transfer to do, we update the line of transfers with the datearrived
1598 if ($datesent) {
1599 if ( $tobranch eq $branch ) {
1600 my $sth = C4::Context->dbh->prepare(
1601 "UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL"
1603 $sth->execute( $item->{'itemnumber'} );
1604 # if we have a reservation with valid transfer, we can set it's status to 'W'
1605 C4::Reserves::ModReserveStatus($item->{'itemnumber'}, 'W');
1606 } else {
1607 $messages->{'WrongTransfer'} = $tobranch;
1608 $messages->{'WrongTransferItem'} = $item->{'itemnumber'};
1610 $validTransfert = 1;
1613 # fix up the accounts.....
1614 if ($item->{'itemlost'}) {
1615 _FixAccountForLostAndReturned($item->{'itemnumber'}, $borrowernumber, $barcode); # can tolerate undef $borrowernumber
1616 $messages->{'WasLost'} = 1;
1619 # fix up the overdues in accounts...
1620 if ($borrowernumber) {
1621 my $fix = _FixOverduesOnReturn($borrowernumber, $item->{itemnumber}, $exemptfine, $dropbox);
1622 defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $item->{itemnumber}...) failed!"; # zero is OK, check defined
1624 # fix fine days
1625 my $debardate = _FixFineDaysOnReturn( $borrower, $item, $issue->{date_due} );
1626 $messages->{'Debarred'} = $debardate if ($debardate);
1629 # find reserves.....
1630 # if we don't have a reserve with the status W, we launch the Checkreserves routine
1631 my ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->{'itemnumber'} );
1632 if ($resfound) {
1633 $resrec->{'ResFound'} = $resfound;
1634 $messages->{'ResFound'} = $resrec;
1637 # update stats?
1638 # Record the fact that this book was returned.
1639 UpdateStats(
1640 $branch, $stat_type, '0', '',
1641 $item->{'itemnumber'},
1642 $biblio->{'itemtype'},
1643 $borrowernumber
1646 # Send a check-in slip. # NOTE: borrower may be undef. probably shouldn't try to send messages then.
1647 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1648 my %conditions = (
1649 branchcode => $branch,
1650 categorycode => $borrower->{categorycode},
1651 item_type => $item->{itype},
1652 notification => 'CHECKIN',
1654 if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
1655 SendCirculationAlert({
1656 type => 'CHECKIN',
1657 item => $item,
1658 borrower => $borrower,
1659 branch => $branch,
1663 logaction("CIRCULATION", "RETURN", $borrowernumber, $item->{'biblionumber'})
1664 if C4::Context->preference("ReturnLog");
1666 # FIXME: make this comment intelligible.
1667 #adding message if holdingbranch is non equal a userenv branch to return the document to homebranch
1668 #we check, if we don't have reserv or transfert for this document, if not, return it to homebranch .
1670 if (($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $hbr) and not $messages->{'WrongTransfer'}){
1671 if ( C4::Context->preference("AutomaticItemReturn" ) or
1672 (C4::Context->preference("UseBranchTransferLimits") and
1673 ! IsBranchTransferAllowed($branch, $hbr, $item->{C4::Context->preference("BranchTransferLimitsType")} )
1674 )) {
1675 $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s)", $item->{'itemnumber'},$branch, $hbr;
1676 $debug and warn "item: " . Dumper($item);
1677 ModItemTransfer($item->{'itemnumber'}, $branch, $hbr);
1678 $messages->{'WasTransfered'} = 1;
1679 } else {
1680 $messages->{'NeedsTransfer'} = 1; # TODO: instead of 1, specify branchcode that the transfer SHOULD go to, $item->{homebranch}
1683 return ( $doreturn, $messages, $issue, $borrower );
1686 =head2 MarkIssueReturned
1688 MarkIssueReturned($borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy);
1690 Unconditionally marks an issue as being returned by
1691 moving the C<issues> row to C<old_issues> and
1692 setting C<returndate> to the current date, or
1693 the last non-holiday date of the branccode specified in
1694 C<dropbox_branch> . Assumes you've already checked that
1695 it's safe to do this, i.e. last non-holiday > issuedate.
1697 if C<$returndate> is specified (in iso format), it is used as the date
1698 of the return. It is ignored when a dropbox_branch is passed in.
1700 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
1701 the old_issue is immediately anonymised
1703 Ideally, this function would be internal to C<C4::Circulation>,
1704 not exported, but it is currently needed by one
1705 routine in C<C4::Accounts>.
1707 =cut
1709 sub MarkIssueReturned {
1710 my ( $borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy ) = @_;
1711 my $dbh = C4::Context->dbh;
1712 my $query = "UPDATE issues SET returndate=";
1713 my @bind;
1714 if ($dropbox_branch) {
1715 my $calendar = C4::Calendar->new( branchcode => $dropbox_branch );
1716 my $dropboxdate = $calendar->addDate( C4::Dates->new(), -1 );
1717 $query .= " ? ";
1718 push @bind, $dropboxdate->output('iso');
1719 } elsif ($returndate) {
1720 $query .= " ? ";
1721 push @bind, $returndate;
1722 } else {
1723 $query .= " now() ";
1725 $query .= " WHERE borrowernumber = ? AND itemnumber = ?";
1726 push @bind, $borrowernumber, $itemnumber;
1727 # FIXME transaction
1728 my $sth_upd = $dbh->prepare($query);
1729 $sth_upd->execute(@bind);
1730 my $sth_copy = $dbh->prepare("INSERT INTO old_issues SELECT * FROM issues
1731 WHERE borrowernumber = ?
1732 AND itemnumber = ?");
1733 $sth_copy->execute($borrowernumber, $itemnumber);
1734 # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
1735 if ( $privacy == 2) {
1736 # The default of 0 does not work due to foreign key constraints
1737 # The anonymisation will fail quietly if AnonymousPatron is not a valid entry
1738 my $anonymouspatron = (C4::Context->preference('AnonymousPatron')) ? C4::Context->preference('AnonymousPatron') : 0;
1739 my $sth_ano = $dbh->prepare("UPDATE old_issues SET borrowernumber=?
1740 WHERE borrowernumber = ?
1741 AND itemnumber = ?");
1742 $sth_ano->execute($anonymouspatron, $borrowernumber, $itemnumber);
1744 my $sth_del = $dbh->prepare("DELETE FROM issues
1745 WHERE borrowernumber = ?
1746 AND itemnumber = ?");
1747 $sth_del->execute($borrowernumber, $itemnumber);
1750 =head2 _FixFineDaysOnReturn
1752 &_FixFineDaysOnReturn($borrower, $item, $datedue);
1754 C<$borrower> borrower hashref
1756 C<$item> item hashref
1758 C<$datedue> date due
1760 Internal function, called only by AddReturn that calculate and update the user fine days, and debars him
1762 =cut
1764 sub _FixFineDaysOnReturn {
1765 my ( $borrower, $item, $datedue ) = @_;
1767 if ($datedue) {
1768 $datedue = C4::Dates->new( $datedue, "iso" );
1769 } else {
1770 return;
1773 my $branchcode = _GetCircControlBranch( $item, $borrower );
1774 my $calendar = C4::Calendar->new( branchcode => $branchcode );
1775 my $today = C4::Dates->new();
1777 my $deltadays = $calendar->daysBetween( $datedue, C4::Dates->new() );
1779 my $circcontrol = C4::Context::preference('CircControl');
1780 my $issuingrule = GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode );
1781 my $finedays = $issuingrule->{finedays};
1783 # exit if no finedays defined
1784 return unless $finedays;
1785 my $grace = $issuingrule->{firstremind};
1787 if ( $deltadays - $grace > 0 ) {
1788 my @newdate = Add_Delta_Days( Today(), $deltadays * $finedays );
1789 my $isonewdate = join( '-', @newdate );
1790 my ( $deby, $debm, $debd ) = split( /-/, $borrower->{debarred} );
1791 if ( check_date( $deby, $debm, $debd ) ) {
1792 my @olddate = split( /-/, $borrower->{debarred} );
1794 if ( Delta_Days( @olddate, @newdate ) > 0 ) {
1795 C4::Members::DebarMember( $borrower->{borrowernumber}, $isonewdate );
1796 return $isonewdate;
1798 } else {
1799 C4::Members::DebarMember( $borrower->{borrowernumber}, $isonewdate );
1800 return $isonewdate;
1805 =head2 _FixOverduesOnReturn
1807 &_FixOverduesOnReturn($brn,$itm, $exemptfine, $dropboxmode);
1809 C<$brn> borrowernumber
1811 C<$itm> itemnumber
1813 C<$exemptfine> BOOL -- remove overdue charge associated with this issue.
1814 C<$dropboxmode> BOOL -- remove lastincrement on overdue charge associated with this issue.
1816 Internal function, called only by AddReturn
1818 =cut
1820 sub _FixOverduesOnReturn {
1821 my ($borrowernumber, $item);
1822 unless ($borrowernumber = shift) {
1823 warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
1824 return;
1826 unless ($item = shift) {
1827 warn "_FixOverduesOnReturn() not supplied valid itemnumber";
1828 return;
1830 my ($exemptfine, $dropbox) = @_;
1831 my $dbh = C4::Context->dbh;
1833 # check for overdue fine
1834 my $sth = $dbh->prepare(
1835 "SELECT * FROM accountlines WHERE (borrowernumber = ?) AND (itemnumber = ?) AND (accounttype='FU' OR accounttype='O')"
1837 $sth->execute( $borrowernumber, $item );
1839 # alter fine to show that the book has been returned
1840 my $data = $sth->fetchrow_hashref;
1841 return 0 unless $data; # no warning, there's just nothing to fix
1843 my $uquery;
1844 my @bind = ($borrowernumber, $item, $data->{'accountno'});
1845 if ($exemptfine) {
1846 $uquery = "update accountlines set accounttype='FFOR', amountoutstanding=0";
1847 if (C4::Context->preference("FinesLog")) {
1848 &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
1850 } elsif ($dropbox && $data->{lastincrement}) {
1851 my $outstanding = $data->{amountoutstanding} - $data->{lastincrement} ;
1852 my $amt = $data->{amount} - $data->{lastincrement} ;
1853 if (C4::Context->preference("FinesLog")) {
1854 &logaction("FINES", 'MODIFY',$borrowernumber,"Dropbox adjustment $amt, item $item");
1856 $uquery = "update accountlines set accounttype='F' ";
1857 if($outstanding >= 0 && $amt >=0) {
1858 $uquery .= ", amount = ? , amountoutstanding=? ";
1859 unshift @bind, ($amt, $outstanding) ;
1861 } else {
1862 $uquery = "update accountlines set accounttype='F' ";
1864 $uquery .= " where (borrowernumber = ?) and (itemnumber = ?) and (accountno = ?)";
1865 my $usth = $dbh->prepare($uquery);
1866 return $usth->execute(@bind);
1869 =head2 _FixAccountForLostAndReturned
1871 &_FixAccountForLostAndReturned($itemnumber, [$borrowernumber, $barcode]);
1873 Calculates the charge for a book lost and returned.
1875 Internal function, not exported, called only by AddReturn.
1877 FIXME: This function reflects how inscrutable fines logic is. Fix both.
1878 FIXME: Give a positive return value on success. It might be the $borrowernumber who received credit, or the amount forgiven.
1880 =cut
1882 sub _FixAccountForLostAndReturned {
1883 my $itemnumber = shift or return;
1884 my $borrowernumber = @_ ? shift : undef;
1885 my $item_id = @_ ? shift : $itemnumber; # Send the barcode if you want that logged in the description
1886 my $dbh = C4::Context->dbh;
1887 # check for charge made for lost book
1888 my $sth = $dbh->prepare("SELECT * FROM accountlines WHERE itemnumber = ? AND accounttype IN ('L', 'Rep', 'W') ORDER BY date DESC, accountno DESC");
1889 $sth->execute($itemnumber);
1890 my $data = $sth->fetchrow_hashref;
1891 $data or return; # bail if there is nothing to do
1892 $data->{accounttype} eq 'W' and return; # Written off
1894 # writeoff this amount
1895 my $offset;
1896 my $amount = $data->{'amount'};
1897 my $acctno = $data->{'accountno'};
1898 my $amountleft; # Starts off undef/zero.
1899 if ($data->{'amountoutstanding'} == $amount) {
1900 $offset = $data->{'amount'};
1901 $amountleft = 0; # Hey, it's zero here, too.
1902 } else {
1903 $offset = $amount - $data->{'amountoutstanding'}; # Um, isn't this the same as ZERO? We just tested those two things are ==
1904 $amountleft = $data->{'amountoutstanding'} - $amount; # Um, isn't this the same as ZERO? We just tested those two things are ==
1906 my $usth = $dbh->prepare("UPDATE accountlines SET accounttype = 'LR',amountoutstanding='0'
1907 WHERE (borrowernumber = ?)
1908 AND (itemnumber = ?) AND (accountno = ?) ");
1909 $usth->execute($data->{'borrowernumber'},$itemnumber,$acctno); # We might be adjusting an account for some OTHER borrowernumber now. Not the one we passed in.
1910 #check if any credit is left if so writeoff other accounts
1911 my $nextaccntno = getnextacctno($data->{'borrowernumber'});
1912 $amountleft *= -1 if ($amountleft < 0);
1913 if ($amountleft > 0) {
1914 my $msth = $dbh->prepare("SELECT * FROM accountlines WHERE (borrowernumber = ?)
1915 AND (amountoutstanding >0) ORDER BY date"); # might want to order by amountoustanding ASC (pay smallest first)
1916 $msth->execute($data->{'borrowernumber'});
1917 # offset transactions
1918 my $newamtos;
1919 my $accdata;
1920 while (($accdata=$msth->fetchrow_hashref) and ($amountleft>0)){
1921 if ($accdata->{'amountoutstanding'} < $amountleft) {
1922 $newamtos = 0;
1923 $amountleft -= $accdata->{'amountoutstanding'};
1924 } else {
1925 $newamtos = $accdata->{'amountoutstanding'} - $amountleft;
1926 $amountleft = 0;
1928 my $thisacct = $accdata->{'accountno'};
1929 # FIXME: move prepares outside while loop!
1930 my $usth = $dbh->prepare("UPDATE accountlines SET amountoutstanding= ?
1931 WHERE (borrowernumber = ?)
1932 AND (accountno=?)");
1933 $usth->execute($newamtos,$data->{'borrowernumber'},'$thisacct'); # FIXME: '$thisacct' is a string literal!
1934 $usth = $dbh->prepare("INSERT INTO accountoffsets
1935 (borrowernumber, accountno, offsetaccount, offsetamount)
1936 VALUES
1937 (?,?,?,?)");
1938 $usth->execute($data->{'borrowernumber'},$accdata->{'accountno'},$nextaccntno,$newamtos);
1940 $msth->finish; # $msth might actually have data left
1942 $amountleft *= -1 if ($amountleft > 0);
1943 my $desc = "Item Returned " . $item_id;
1944 $usth = $dbh->prepare("INSERT INTO accountlines
1945 (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
1946 VALUES (?,?,now(),?,?,'CR',?)");
1947 $usth->execute($data->{'borrowernumber'},$nextaccntno,0-$amount,$desc,$amountleft);
1948 if ($borrowernumber) {
1949 # FIXME: same as query above. use 1 sth for both
1950 $usth = $dbh->prepare("INSERT INTO accountoffsets
1951 (borrowernumber, accountno, offsetaccount, offsetamount)
1952 VALUES (?,?,?,?)");
1953 $usth->execute($borrowernumber, $data->{'accountno'}, $nextaccntno, $offset);
1955 ModItem({ paidfor => '' }, undef, $itemnumber);
1956 return;
1959 =head2 _GetCircControlBranch
1961 my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
1963 Internal function :
1965 Return the library code to be used to determine which circulation
1966 policy applies to a transaction. Looks up the CircControl and
1967 HomeOrHoldingBranch system preferences.
1969 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
1971 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
1973 =cut
1975 sub _GetCircControlBranch {
1976 my ($item, $borrower) = @_;
1977 my $circcontrol = C4::Context->preference('CircControl');
1978 my $branch;
1980 if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
1981 $branch= C4::Context->userenv->{'branch'};
1982 } elsif ($circcontrol eq 'PatronLibrary') {
1983 $branch=$borrower->{branchcode};
1984 } else {
1985 my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
1986 $branch = $item->{$branchfield};
1987 # default to item home branch if holdingbranch is used
1988 # and is not defined
1989 if (!defined($branch) && $branchfield eq 'holdingbranch') {
1990 $branch = $item->{homebranch};
1993 return $branch;
2001 =head2 GetItemIssue
2003 $issue = &GetItemIssue($itemnumber);
2005 Returns patron currently having a book, or undef if not checked out.
2007 C<$itemnumber> is the itemnumber.
2009 C<$issue> is a hashref of the row from the issues table.
2011 =cut
2013 sub GetItemIssue {
2014 my ($itemnumber) = @_;
2015 return unless $itemnumber;
2016 my $sth = C4::Context->dbh->prepare(
2017 "SELECT *
2018 FROM issues
2019 LEFT JOIN items ON issues.itemnumber=items.itemnumber
2020 WHERE issues.itemnumber=?");
2021 $sth->execute($itemnumber);
2022 my $data = $sth->fetchrow_hashref;
2023 return unless $data;
2024 $data->{'overdue'} = ($data->{'date_due'} lt C4::Dates->today('iso')) ? 1 : 0;
2025 return ($data);
2028 =head2 GetOpenIssue
2030 $issue = GetOpenIssue( $itemnumber );
2032 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2034 C<$itemnumber> is the item's itemnumber
2036 Returns a hashref
2038 =cut
2040 sub GetOpenIssue {
2041 my ( $itemnumber ) = @_;
2043 my $dbh = C4::Context->dbh;
2044 my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2045 $sth->execute( $itemnumber );
2046 my $issue = $sth->fetchrow_hashref();
2047 return $issue;
2050 =head2 GetItemIssues
2052 $issues = &GetItemIssues($itemnumber, $history);
2054 Returns patrons that have issued a book
2056 C<$itemnumber> is the itemnumber
2057 C<$history> is false if you just want the current "issuer" (if any)
2058 and true if you want issues history from old_issues also.
2060 Returns reference to an array of hashes
2062 =cut
2064 sub GetItemIssues {
2065 my ( $itemnumber, $history ) = @_;
2067 my $today = C4::Dates->today('iso'); # get today date
2068 my $sql = "SELECT * FROM issues
2069 JOIN borrowers USING (borrowernumber)
2070 JOIN items USING (itemnumber)
2071 WHERE issues.itemnumber = ? ";
2072 if ($history) {
2073 $sql .= "UNION ALL
2074 SELECT * FROM old_issues
2075 LEFT JOIN borrowers USING (borrowernumber)
2076 JOIN items USING (itemnumber)
2077 WHERE old_issues.itemnumber = ? ";
2079 $sql .= "ORDER BY date_due DESC";
2080 my $sth = C4::Context->dbh->prepare($sql);
2081 if ($history) {
2082 $sth->execute($itemnumber, $itemnumber);
2083 } else {
2084 $sth->execute($itemnumber);
2086 my $results = $sth->fetchall_arrayref({});
2087 foreach (@$results) {
2088 $_->{'overdue'} = ($_->{'date_due'} lt $today) ? 1 : 0;
2090 return $results;
2093 =head2 GetBiblioIssues
2095 $issues = GetBiblioIssues($biblionumber);
2097 this function get all issues from a biblionumber.
2099 Return:
2100 C<$issues> is a reference to array which each value is ref-to-hash. This ref-to-hash containts all column from
2101 tables issues and the firstname,surname & cardnumber from borrowers.
2103 =cut
2105 sub GetBiblioIssues {
2106 my $biblionumber = shift;
2107 return undef unless $biblionumber;
2108 my $dbh = C4::Context->dbh;
2109 my $query = "
2110 SELECT issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2111 FROM issues
2112 LEFT JOIN borrowers ON borrowers.borrowernumber = issues.borrowernumber
2113 LEFT JOIN items ON issues.itemnumber = items.itemnumber
2114 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2115 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2116 WHERE biblio.biblionumber = ?
2117 UNION ALL
2118 SELECT old_issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2119 FROM old_issues
2120 LEFT JOIN borrowers ON borrowers.borrowernumber = old_issues.borrowernumber
2121 LEFT JOIN items ON old_issues.itemnumber = items.itemnumber
2122 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2123 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2124 WHERE biblio.biblionumber = ?
2125 ORDER BY timestamp
2127 my $sth = $dbh->prepare($query);
2128 $sth->execute($biblionumber, $biblionumber);
2130 my @issues;
2131 while ( my $data = $sth->fetchrow_hashref ) {
2132 push @issues, $data;
2134 return \@issues;
2137 =head2 GetUpcomingDueIssues
2139 my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2141 =cut
2143 sub GetUpcomingDueIssues {
2144 my $params = shift;
2146 $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2147 my $dbh = C4::Context->dbh;
2149 my $statement = <<END_SQL;
2150 SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2151 FROM issues
2152 LEFT JOIN items USING (itemnumber)
2153 LEFT OUTER JOIN branches USING (branchcode)
2154 WhERE returndate is NULL
2155 AND ( TO_DAYS( NOW() )-TO_DAYS( date_due ) ) < ?
2156 END_SQL
2158 my @bind_parameters = ( $params->{'days_in_advance'} );
2160 my $sth = $dbh->prepare( $statement );
2161 $sth->execute( @bind_parameters );
2162 my $upcoming_dues = $sth->fetchall_arrayref({});
2163 $sth->finish;
2165 return $upcoming_dues;
2168 =head2 CanBookBeRenewed
2170 ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber[, $override_limit]);
2172 Find out whether a borrowed item may be renewed.
2174 C<$dbh> is a DBI handle to the Koha database.
2176 C<$borrowernumber> is the borrower number of the patron who currently
2177 has the item on loan.
2179 C<$itemnumber> is the number of the item to renew.
2181 C<$override_limit>, if supplied with a true value, causes
2182 the limit on the number of times that the loan can be renewed
2183 (as controlled by the item type) to be ignored.
2185 C<$CanBookBeRenewed> returns a true value iff the item may be renewed. The
2186 item must currently be on loan to the specified borrower; renewals
2187 must be allowed for the item's type; and the borrower must not have
2188 already renewed the loan. $error will contain the reason the renewal can not proceed
2190 =cut
2192 sub CanBookBeRenewed {
2194 # check renewal status
2195 my ( $borrowernumber, $itemnumber, $override_limit ) = @_;
2196 my $dbh = C4::Context->dbh;
2197 my $renews = 1;
2198 my $renewokay = 0;
2199 my $error;
2201 # Look in the issues table for this item, lent to this borrower,
2202 # and not yet returned.
2204 # Look in the issues table for this item, lent to this borrower,
2205 # and not yet returned.
2206 my %branch = (
2207 'ItemHomeLibrary' => 'items.homebranch',
2208 'PickupLibrary' => 'items.holdingbranch',
2209 'PatronLibrary' => 'borrowers.branchcode'
2211 my $controlbranch = $branch{C4::Context->preference('CircControl')};
2212 my $itype = C4::Context->preference('item-level_itypes') ? 'items.itype' : 'biblioitems.itemtype';
2214 my $sthcount = $dbh->prepare("
2215 SELECT
2216 borrowers.categorycode, biblioitems.itemtype, issues.renewals, renewalsallowed, $controlbranch
2217 FROM issuingrules,
2218 issues
2219 LEFT JOIN items USING (itemnumber)
2220 LEFT JOIN borrowers USING (borrowernumber)
2221 LEFT JOIN biblioitems USING (biblioitemnumber)
2223 WHERE
2224 (issuingrules.categorycode = borrowers.categorycode OR issuingrules.categorycode = '*')
2226 (issuingrules.itemtype = $itype OR issuingrules.itemtype = '*')
2228 (issuingrules.branchcode = $controlbranch OR issuingrules.branchcode = '*')
2229 AND
2230 borrowernumber = ?
2232 itemnumber = ?
2233 ORDER BY
2234 issuingrules.categorycode desc,
2235 issuingrules.itemtype desc,
2236 issuingrules.branchcode desc
2237 LIMIT 1;
2240 $sthcount->execute( $borrowernumber, $itemnumber );
2241 if ( my $data1 = $sthcount->fetchrow_hashref ) {
2243 if ( ( $data1->{renewalsallowed} && $data1->{renewalsallowed} > $data1->{renewals} ) || $override_limit ) {
2244 $renewokay = 1;
2246 else {
2247 $error="too_many";
2250 my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2251 if ($resfound) {
2252 $renewokay = 0;
2253 $error="on_reserve"
2257 return ($renewokay,$error);
2260 =head2 AddRenewal
2262 &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
2264 Renews a loan.
2266 C<$borrowernumber> is the borrower number of the patron who currently
2267 has the item.
2269 C<$itemnumber> is the number of the item to renew.
2271 C<$branch> is the library where the renewal took place (if any).
2272 The library that controls the circ policies for the renewal is retrieved from the issues record.
2274 C<$datedue> can be a C4::Dates object used to set the due date.
2276 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate. If
2277 this parameter is not supplied, lastreneweddate is set to the current date.
2279 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2280 from the book's item type.
2282 =cut
2284 sub AddRenewal {
2285 my $borrowernumber = shift or return undef;
2286 my $itemnumber = shift or return undef;
2287 my $branch = shift;
2288 my $datedue = shift;
2289 my $lastreneweddate = shift || C4::Dates->new()->output('iso');
2290 my $item = GetItem($itemnumber) or return undef;
2291 my $biblio = GetBiblioFromItemNumber($itemnumber) or return undef;
2293 my $dbh = C4::Context->dbh;
2294 # Find the issues record for this book
2295 my $sth =
2296 $dbh->prepare("SELECT * FROM issues
2297 WHERE borrowernumber=?
2298 AND itemnumber=?"
2300 $sth->execute( $borrowernumber, $itemnumber );
2301 my $issuedata = $sth->fetchrow_hashref;
2302 $sth->finish;
2303 if($datedue && ! $datedue->output('iso')){
2304 warn "Invalid date passed to AddRenewal.";
2305 return undef;
2307 # If the due date wasn't specified, calculate it by adding the
2308 # book's loan length to today's date or the current due date
2309 # based on the value of the RenewalPeriodBase syspref.
2310 unless ($datedue) {
2312 my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber ) or return undef;
2313 my $itemtype = (C4::Context->preference('item-level_itypes')) ? $biblio->{'itype'} : $biblio->{'itemtype'};
2315 $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
2316 C4::Dates->new($issuedata->{date_due}, 'iso') :
2317 C4::Dates->new();
2318 $datedue = CalcDateDue($datedue,$itemtype,$issuedata->{'branchcode'},$borrower);
2321 # Update the issues record to have the new due date, and a new count
2322 # of how many times it has been renewed.
2323 my $renews = $issuedata->{'renewals'} + 1;
2324 $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
2325 WHERE borrowernumber=?
2326 AND itemnumber=?"
2328 $sth->execute( $datedue->output('iso'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
2329 $sth->finish;
2331 # Update the renewal count on the item, and tell zebra to reindex
2332 $renews = $biblio->{'renewals'} + 1;
2333 ModItem({ renewals => $renews, onloan => $datedue->output('iso') }, $biblio->{'biblionumber'}, $itemnumber);
2335 # Charge a new rental fee, if applicable?
2336 my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
2337 if ( $charge > 0 ) {
2338 my $accountno = getnextacctno( $borrowernumber );
2339 my $item = GetBiblioFromItemNumber($itemnumber);
2340 my $manager_id = 0;
2341 $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
2342 $sth = $dbh->prepare(
2343 "INSERT INTO accountlines
2344 (date, borrowernumber, accountno, amount, manager_id,
2345 description,accounttype, amountoutstanding, itemnumber)
2346 VALUES (now(),?,?,?,?,?,?,?,?)"
2348 $sth->execute( $borrowernumber, $accountno, $charge, $manager_id,
2349 "Renewal of Rental Item $item->{'title'} $item->{'barcode'}",
2350 'Rent', $charge, $itemnumber );
2351 $sth->finish;
2353 # Log the renewal
2354 UpdateStats( $branch, 'renew', $charge, '', $itemnumber, $item->{itype}, $borrowernumber);
2355 return $datedue;
2358 sub GetRenewCount {
2359 # check renewal status
2360 my ( $bornum, $itemno ) = @_;
2361 my $dbh = C4::Context->dbh;
2362 my $renewcount = 0;
2363 my $renewsallowed = 0;
2364 my $renewsleft = 0;
2366 my $borrower = C4::Members::GetMember( borrowernumber => $bornum);
2367 my $item = GetItem($itemno);
2369 # Look in the issues table for this item, lent to this borrower,
2370 # and not yet returned.
2372 # FIXME - I think this function could be redone to use only one SQL call.
2373 my $sth = $dbh->prepare(
2374 "select * from issues
2375 where (borrowernumber = ?)
2376 and (itemnumber = ?)"
2378 $sth->execute( $bornum, $itemno );
2379 my $data = $sth->fetchrow_hashref;
2380 $renewcount = $data->{'renewals'} if $data->{'renewals'};
2381 $sth->finish;
2382 # $item and $borrower should be calculated
2383 my $branchcode = _GetCircControlBranch($item, $borrower);
2385 my $issuingrule = GetIssuingRule($borrower->{categorycode}, $item->{itype}, $branchcode);
2387 $renewsallowed = $issuingrule->{'renewalsallowed'};
2388 $renewsleft = $renewsallowed - $renewcount;
2389 if($renewsleft < 0){ $renewsleft = 0; }
2390 return ( $renewcount, $renewsallowed, $renewsleft );
2393 =head2 GetIssuingCharges
2395 ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
2397 Calculate how much it would cost for a given patron to borrow a given
2398 item, including any applicable discounts.
2400 C<$itemnumber> is the item number of item the patron wishes to borrow.
2402 C<$borrowernumber> is the patron's borrower number.
2404 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
2405 and C<$item_type> is the code for the item's item type (e.g., C<VID>
2406 if it's a video).
2408 =cut
2410 sub GetIssuingCharges {
2412 # calculate charges due
2413 my ( $itemnumber, $borrowernumber ) = @_;
2414 my $charge = 0;
2415 my $dbh = C4::Context->dbh;
2416 my $item_type;
2418 # Get the book's item type and rental charge (via its biblioitem).
2419 my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
2420 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
2421 $charge_query .= (C4::Context->preference('item-level_itypes'))
2422 ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
2423 : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
2425 $charge_query .= ' WHERE items.itemnumber =?';
2427 my $sth = $dbh->prepare($charge_query);
2428 $sth->execute($itemnumber);
2429 if ( my $item_data = $sth->fetchrow_hashref ) {
2430 $item_type = $item_data->{itemtype};
2431 $charge = $item_data->{rentalcharge};
2432 my $branch = C4::Branch::mybranch();
2433 my $discount_query = q|SELECT rentaldiscount,
2434 issuingrules.itemtype, issuingrules.branchcode
2435 FROM borrowers
2436 LEFT JOIN issuingrules ON borrowers.categorycode = issuingrules.categorycode
2437 WHERE borrowers.borrowernumber = ?
2438 AND (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
2439 AND (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')|;
2440 my $discount_sth = $dbh->prepare($discount_query);
2441 $discount_sth->execute( $borrowernumber, $item_type, $branch );
2442 my $discount_rules = $discount_sth->fetchall_arrayref({});
2443 if (@{$discount_rules}) {
2444 # We may have multiple rules so get the most specific
2445 my $discount = _get_discount_from_rule($discount_rules, $branch, $item_type);
2446 $charge = ( $charge * ( 100 - $discount ) ) / 100;
2450 $sth->finish; # we havent _explicitly_ fetched all rows
2451 return ( $charge, $item_type );
2454 # Select most appropriate discount rule from those returned
2455 sub _get_discount_from_rule {
2456 my ($rules_ref, $branch, $itemtype) = @_;
2457 my $discount;
2459 if (@{$rules_ref} == 1) { # only 1 applicable rule use it
2460 $discount = $rules_ref->[0]->{rentaldiscount};
2461 return (defined $discount) ? $discount : 0;
2463 # could have up to 4 does one match $branch and $itemtype
2464 my @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq $itemtype } @{$rules_ref};
2465 if (@d) {
2466 $discount = $d[0]->{rentaldiscount};
2467 return (defined $discount) ? $discount : 0;
2469 # do we have item type + all branches
2470 @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq $itemtype } @{$rules_ref};
2471 if (@d) {
2472 $discount = $d[0]->{rentaldiscount};
2473 return (defined $discount) ? $discount : 0;
2475 # do we all item types + this branch
2476 @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq q{*} } @{$rules_ref};
2477 if (@d) {
2478 $discount = $d[0]->{rentaldiscount};
2479 return (defined $discount) ? $discount : 0;
2481 # so all and all (surely we wont get here)
2482 @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq q{*} } @{$rules_ref};
2483 if (@d) {
2484 $discount = $d[0]->{rentaldiscount};
2485 return (defined $discount) ? $discount : 0;
2487 # none of the above
2488 return 0;
2491 =head2 AddIssuingCharge
2493 &AddIssuingCharge( $itemno, $borrowernumber, $charge )
2495 =cut
2497 sub AddIssuingCharge {
2498 my ( $itemnumber, $borrowernumber, $charge ) = @_;
2499 my $dbh = C4::Context->dbh;
2500 my $nextaccntno = getnextacctno( $borrowernumber );
2501 my $manager_id = 0;
2502 $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
2503 my $query ="
2504 INSERT INTO accountlines
2505 (borrowernumber, itemnumber, accountno,
2506 date, amount, description, accounttype,
2507 amountoutstanding, manager_id)
2508 VALUES (?, ?, ?,now(), ?, 'Rental', 'Rent',?,?)
2510 my $sth = $dbh->prepare($query);
2511 $sth->execute( $borrowernumber, $itemnumber, $nextaccntno, $charge, $charge, $manager_id );
2512 $sth->finish;
2515 =head2 GetTransfers
2517 GetTransfers($itemnumber);
2519 =cut
2521 sub GetTransfers {
2522 my ($itemnumber) = @_;
2524 my $dbh = C4::Context->dbh;
2526 my $query = '
2527 SELECT datesent,
2528 frombranch,
2529 tobranch
2530 FROM branchtransfers
2531 WHERE itemnumber = ?
2532 AND datearrived IS NULL
2534 my $sth = $dbh->prepare($query);
2535 $sth->execute($itemnumber);
2536 my @row = $sth->fetchrow_array();
2537 $sth->finish;
2538 return @row;
2541 =head2 GetTransfersFromTo
2543 @results = GetTransfersFromTo($frombranch,$tobranch);
2545 Returns the list of pending transfers between $from and $to branch
2547 =cut
2549 sub GetTransfersFromTo {
2550 my ( $frombranch, $tobranch ) = @_;
2551 return unless ( $frombranch && $tobranch );
2552 my $dbh = C4::Context->dbh;
2553 my $query = "
2554 SELECT itemnumber,datesent,frombranch
2555 FROM branchtransfers
2556 WHERE frombranch=?
2557 AND tobranch=?
2558 AND datearrived IS NULL
2560 my $sth = $dbh->prepare($query);
2561 $sth->execute( $frombranch, $tobranch );
2562 my @gettransfers;
2564 while ( my $data = $sth->fetchrow_hashref ) {
2565 push @gettransfers, $data;
2567 $sth->finish;
2568 return (@gettransfers);
2571 =head2 DeleteTransfer
2573 &DeleteTransfer($itemnumber);
2575 =cut
2577 sub DeleteTransfer {
2578 my ($itemnumber) = @_;
2579 my $dbh = C4::Context->dbh;
2580 my $sth = $dbh->prepare(
2581 "DELETE FROM branchtransfers
2582 WHERE itemnumber=?
2583 AND datearrived IS NULL "
2585 $sth->execute($itemnumber);
2586 $sth->finish;
2589 =head2 AnonymiseIssueHistory
2591 $rows = AnonymiseIssueHistory($date,$borrowernumber)
2593 This function write NULL instead of C<$borrowernumber> given on input arg into the table issues.
2594 if C<$borrowernumber> is not set, it will delete the issue history for all borrower older than C<$date>.
2596 If c<$borrowernumber> is set, it will delete issue history for only that borrower, regardless of their opac privacy
2597 setting (force delete).
2599 return the number of affected rows.
2601 =cut
2603 sub AnonymiseIssueHistory {
2604 my $date = shift;
2605 my $borrowernumber = shift;
2606 my $dbh = C4::Context->dbh;
2607 my $query = "
2608 UPDATE old_issues
2609 SET borrowernumber = ?
2610 WHERE returndate < ?
2611 AND borrowernumber IS NOT NULL
2614 # The default of 0 does not work due to foreign key constraints
2615 # The anonymisation will fail quietly if AnonymousPatron is not a valid entry
2616 my $anonymouspatron = (C4::Context->preference('AnonymousPatron')) ? C4::Context->preference('AnonymousPatron') : 0;
2617 my @bind_params = ($anonymouspatron, $date);
2618 if (defined $borrowernumber) {
2619 $query .= " AND borrowernumber = ?";
2620 push @bind_params, $borrowernumber;
2621 } else {
2622 $query .= " AND (SELECT privacy FROM borrowers WHERE borrowers.borrowernumber=old_issues.borrowernumber) <> 0";
2624 my $sth = $dbh->prepare($query);
2625 $sth->execute(@bind_params);
2626 my $rows_affected = $sth->rows; ### doublecheck row count return function
2627 return $rows_affected;
2630 =head2 SendCirculationAlert
2632 Send out a C<check-in> or C<checkout> alert using the messaging system.
2634 B<Parameters>:
2636 =over 4
2638 =item type
2640 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
2642 =item item
2644 Hashref of information about the item being checked in or out.
2646 =item borrower
2648 Hashref of information about the borrower of the item.
2650 =item branch
2652 The branchcode from where the checkout or check-in took place.
2654 =back
2656 B<Example>:
2658 SendCirculationAlert({
2659 type => 'CHECKOUT',
2660 item => $item,
2661 borrower => $borrower,
2662 branch => $branch,
2665 =cut
2667 sub SendCirculationAlert {
2668 my ($opts) = @_;
2669 my ($type, $item, $borrower, $branch) =
2670 ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
2671 my %message_name = (
2672 CHECKIN => 'Item_Check_in',
2673 CHECKOUT => 'Item_Checkout',
2675 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
2676 borrowernumber => $borrower->{borrowernumber},
2677 message_name => $message_name{$type},
2679 my $letter = C4::Letters::getletter('circulation', $type);
2680 C4::Letters::parseletter($letter, 'biblio', $item->{biblionumber});
2681 C4::Letters::parseletter($letter, 'biblioitems', $item->{biblionumber});
2682 C4::Letters::parseletter($letter, 'borrowers', $borrower->{borrowernumber});
2683 C4::Letters::parseletter($letter, 'branches', $branch);
2684 my @transports = @{ $borrower_preferences->{transports} };
2685 # warn "no transports" unless @transports;
2686 for (@transports) {
2687 # warn "transport: $_";
2688 my $message = C4::Message->find_last_message($borrower, $type, $_);
2689 if (!$message) {
2690 #warn "create new message";
2691 C4::Message->enqueue($letter, $borrower, $_);
2692 } else {
2693 #warn "append to old message";
2694 $message->append($letter);
2695 $message->update;
2698 $letter;
2701 =head2 updateWrongTransfer
2703 $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
2705 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
2707 =cut
2709 sub updateWrongTransfer {
2710 my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
2711 my $dbh = C4::Context->dbh;
2712 # first step validate the actual line of transfert .
2713 my $sth =
2714 $dbh->prepare(
2715 "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
2717 $sth->execute($FromLibrary,$itemNumber);
2718 $sth->finish;
2720 # second step create a new line of branchtransfer to the right location .
2721 ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
2723 #third step changing holdingbranch of item
2724 UpdateHoldingbranch($FromLibrary,$itemNumber);
2727 =head2 UpdateHoldingbranch
2729 $items = UpdateHoldingbranch($branch,$itmenumber);
2731 Simple methode for updating hodlingbranch in items BDD line
2733 =cut
2735 sub UpdateHoldingbranch {
2736 my ( $branch,$itemnumber ) = @_;
2737 ModItem({ holdingbranch => $branch }, undef, $itemnumber);
2740 =head2 CalcDateDue
2742 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
2744 this function calculates the due date given the start date and configured circulation rules,
2745 checking against the holidays calendar as per the 'useDaysMode' syspref.
2746 C<$startdate> = C4::Dates object representing start date of loan period (assumed to be today)
2747 C<$itemtype> = itemtype code of item in question
2748 C<$branch> = location whose calendar to use
2749 C<$borrower> = Borrower object
2751 =cut
2753 sub CalcDateDue {
2754 my ($startdate,$itemtype,$branch,$borrower) = @_;
2755 my $datedue;
2756 my $loanlength = GetLoanLength($borrower->{'categorycode'},$itemtype, $branch);
2758 # if globalDueDate ON the datedue is set to that date
2759 if ( C4::Context->preference('globalDueDate')
2760 && ( C4::Context->preference('globalDueDate') =~ C4::Dates->regexp('syspref') ) ) {
2761 $datedue = C4::Dates->new( C4::Context->preference('globalDueDate') );
2762 } else {
2763 # otherwise, calculate the datedue as normal
2764 if(C4::Context->preference('useDaysMode') eq 'Days') { # ignoring calendar
2765 my $timedue = time + ($loanlength) * 86400;
2766 #FIXME - assumes now even though we take a startdate
2767 my @datearr = localtime($timedue);
2768 $datedue = C4::Dates->new( sprintf("%04d-%02d-%02d", 1900 + $datearr[5], $datearr[4] + 1, $datearr[3]), 'iso');
2769 } else {
2770 my $calendar = C4::Calendar->new( branchcode => $branch );
2771 $datedue = $calendar->addDate($startdate, $loanlength);
2775 # if Hard Due Dates are used, retreive them and apply as necessary
2776 my ($hardduedate, $hardduedatecompare) = GetHardDueDate($borrower->{'categorycode'},$itemtype, $branch);
2777 if ( $hardduedate && $hardduedate->output('iso') && $hardduedate->output('iso') ne '0000-00-00') {
2778 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
2779 if ( $datedue->output( 'iso' ) gt $hardduedate->output( 'iso' ) && $hardduedatecompare == -1) {
2780 $datedue = $hardduedate;
2781 # if the calculated date is before the 'after' Hard Due Date (floor), override
2782 } elsif ( $datedue->output( 'iso' ) lt $hardduedate->output( 'iso' ) && $hardduedatecompare == 1) {
2783 $datedue = $hardduedate;
2784 # if the hard due date is set to 'exactly', overrride
2785 } elsif ( $hardduedatecompare == 0) {
2786 $datedue = $hardduedate;
2788 # in all other cases, keep the date due as it is
2791 # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
2792 if ( C4::Context->preference('ReturnBeforeExpiry') && $datedue->output('iso') gt $borrower->{dateexpiry} ) {
2793 $datedue = C4::Dates->new( $borrower->{dateexpiry}, 'iso' );
2796 return $datedue;
2799 =head2 CheckValidDatedue
2801 $newdatedue = CheckValidDatedue($date_due,$itemnumber,$branchcode);
2803 This function does not account for holiday exceptions nor does it handle the 'useDaysMode' syspref .
2804 To be replaced by CalcDateDue() once C4::Calendar use is tested.
2806 this function validates the loan length against the holidays calendar, and adjusts the due date as per the 'useDaysMode' syspref.
2807 C<$date_due> = returndate calculate with no day check
2808 C<$itemnumber> = itemnumber
2809 C<$branchcode> = location of issue (affected by 'CircControl' syspref)
2810 C<$loanlength> = loan length prior to adjustment
2812 =cut
2814 sub CheckValidDatedue {
2815 my ($date_due,$itemnumber,$branchcode)=@_;
2816 my @datedue=split('-',$date_due->output('iso'));
2817 my $years=$datedue[0];
2818 my $month=$datedue[1];
2819 my $day=$datedue[2];
2820 # die "Item# $itemnumber ($branchcode) due: " . ${date_due}->output() . "\n(Y,M,D) = ($years,$month,$day)":
2821 my $dow;
2822 for (my $i=0;$i<2;$i++){
2823 $dow=Day_of_Week($years,$month,$day);
2824 ($dow=0) if ($dow>6);
2825 my $result=CheckRepeatableHolidays($itemnumber,$dow,$branchcode);
2826 my $countspecial=CheckSpecialHolidays($years,$month,$day,$itemnumber,$branchcode);
2827 my $countspecialrepeatable=CheckRepeatableSpecialHolidays($month,$day,$itemnumber,$branchcode);
2828 if (($result ne '0') or ($countspecial ne '0') or ($countspecialrepeatable ne '0') ){
2829 $i=0;
2830 (($years,$month,$day) = Add_Delta_Days($years,$month,$day, 1))if ($i ne '1');
2833 my $newdatedue=C4::Dates->new(sprintf("%04d-%02d-%02d",$years,$month,$day),'iso');
2834 return $newdatedue;
2838 =head2 CheckRepeatableHolidays
2840 $countrepeatable = CheckRepeatableHoliday($itemnumber,$week_day,$branchcode);
2842 This function checks if the date due is a repeatable holiday
2844 C<$date_due> = returndate calculate with no day check
2845 C<$itemnumber> = itemnumber
2846 C<$branchcode> = localisation of issue
2848 =cut
2850 sub CheckRepeatableHolidays{
2851 my($itemnumber,$week_day,$branchcode)=@_;
2852 my $dbh = C4::Context->dbh;
2853 my $query = qq|SELECT count(*)
2854 FROM repeatable_holidays
2855 WHERE branchcode=?
2856 AND weekday=?|;
2857 my $sth = $dbh->prepare($query);
2858 $sth->execute($branchcode,$week_day);
2859 my $result=$sth->fetchrow;
2860 $sth->finish;
2861 return $result;
2865 =head2 CheckSpecialHolidays
2867 $countspecial = CheckSpecialHolidays($years,$month,$day,$itemnumber,$branchcode);
2869 This function check if the date is a special holiday
2871 C<$years> = the years of datedue
2872 C<$month> = the month of datedue
2873 C<$day> = the day of datedue
2874 C<$itemnumber> = itemnumber
2875 C<$branchcode> = localisation of issue
2877 =cut
2879 sub CheckSpecialHolidays{
2880 my ($years,$month,$day,$itemnumber,$branchcode) = @_;
2881 my $dbh = C4::Context->dbh;
2882 my $query=qq|SELECT count(*)
2883 FROM `special_holidays`
2884 WHERE year=?
2885 AND month=?
2886 AND day=?
2887 AND branchcode=?
2889 my $sth = $dbh->prepare($query);
2890 $sth->execute($years,$month,$day,$branchcode);
2891 my $countspecial=$sth->fetchrow ;
2892 $sth->finish;
2893 return $countspecial;
2896 =head2 CheckRepeatableSpecialHolidays
2898 $countspecial = CheckRepeatableSpecialHolidays($month,$day,$itemnumber,$branchcode);
2900 This function check if the date is a repeatble special holidays
2902 C<$month> = the month of datedue
2903 C<$day> = the day of datedue
2904 C<$itemnumber> = itemnumber
2905 C<$branchcode> = localisation of issue
2907 =cut
2909 sub CheckRepeatableSpecialHolidays{
2910 my ($month,$day,$itemnumber,$branchcode) = @_;
2911 my $dbh = C4::Context->dbh;
2912 my $query=qq|SELECT count(*)
2913 FROM `repeatable_holidays`
2914 WHERE month=?
2915 AND day=?
2916 AND branchcode=?
2918 my $sth = $dbh->prepare($query);
2919 $sth->execute($month,$day,$branchcode);
2920 my $countspecial=$sth->fetchrow ;
2921 $sth->finish;
2922 return $countspecial;
2927 sub CheckValidBarcode{
2928 my ($barcode) = @_;
2929 my $dbh = C4::Context->dbh;
2930 my $query=qq|SELECT count(*)
2931 FROM items
2932 WHERE barcode=?
2934 my $sth = $dbh->prepare($query);
2935 $sth->execute($barcode);
2936 my $exist=$sth->fetchrow ;
2937 $sth->finish;
2938 return $exist;
2941 =head2 IsBranchTransferAllowed
2943 $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
2945 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
2947 =cut
2949 sub IsBranchTransferAllowed {
2950 my ( $toBranch, $fromBranch, $code ) = @_;
2952 if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
2954 my $limitType = C4::Context->preference("BranchTransferLimitsType");
2955 my $dbh = C4::Context->dbh;
2957 my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
2958 $sth->execute( $toBranch, $fromBranch, $code );
2959 my $limit = $sth->fetchrow_hashref();
2961 ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
2962 if ( $limit->{'limitId'} ) {
2963 return 0;
2964 } else {
2965 return 1;
2969 =head2 CreateBranchTransferLimit
2971 CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
2973 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
2975 =cut
2977 sub CreateBranchTransferLimit {
2978 my ( $toBranch, $fromBranch, $code ) = @_;
2980 my $limitType = C4::Context->preference("BranchTransferLimitsType");
2982 my $dbh = C4::Context->dbh;
2984 my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
2985 $sth->execute( $code, $toBranch, $fromBranch );
2988 =head2 DeleteBranchTransferLimits
2990 DeleteBranchTransferLimits($frombranch);
2992 Deletes all the branch transfer limits for one branch
2994 =cut
2996 sub DeleteBranchTransferLimits {
2997 my $branch = shift;
2998 my $dbh = C4::Context->dbh;
2999 my $sth = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3000 $sth->execute($branch);
3003 sub ReturnLostItem{
3004 my ( $borrowernumber, $itemnum ) = @_;
3006 MarkIssueReturned( $borrowernumber, $itemnum );
3007 my $borrower = C4::Members::GetMember( 'borrowernumber'=>$borrowernumber );
3008 my @datearr = localtime(time);
3009 my $date = ( 1900 + $datearr[5] ) . "-" . ( $datearr[4] + 1 ) . "-" . $datearr[3];
3010 my $bor = "$borrower->{'firstname'} $borrower->{'surname'} $borrower->{'cardnumber'}";
3011 ModItem({ paidfor => "Paid for by $bor $date" }, undef, $itemnum);
3015 sub LostItem{
3016 my ($itemnumber, $mark_returned, $charge_fee) = @_;
3018 my $dbh = C4::Context->dbh();
3019 my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title
3020 FROM issues
3021 JOIN items USING (itemnumber)
3022 JOIN biblio USING (biblionumber)
3023 WHERE issues.itemnumber=?");
3024 $sth->execute($itemnumber);
3025 my $issues=$sth->fetchrow_hashref();
3026 $sth->finish;
3028 # if a borrower lost the item, add a replacement cost to the their record
3029 if ( my $borrowernumber = $issues->{borrowernumber} ){
3031 C4::Accounts::chargelostitem($borrowernumber, $itemnumber, $issues->{'replacementprice'}, "Lost Item $issues->{'title'} $issues->{'barcode'}")
3032 if $charge_fee;
3033 #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3034 #warn " $issues->{'borrowernumber'} / $itemnumber ";
3035 MarkIssueReturned($borrowernumber,$itemnumber) if $mark_returned;
3039 sub GetOfflineOperations {
3040 my $dbh = C4::Context->dbh;
3041 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3042 $sth->execute(C4::Context->userenv->{'branch'});
3043 my $results = $sth->fetchall_arrayref({});
3044 $sth->finish;
3045 return $results;
3048 sub GetOfflineOperation {
3049 my $dbh = C4::Context->dbh;
3050 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3051 $sth->execute( shift );
3052 my $result = $sth->fetchrow_hashref;
3053 $sth->finish;
3054 return $result;
3057 sub AddOfflineOperation {
3058 my $dbh = C4::Context->dbh;
3059 my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber) VALUES(?,?,?,?,?,?)");
3060 $sth->execute( @_ );
3061 return "Added.";
3064 sub DeleteOfflineOperation {
3065 my $dbh = C4::Context->dbh;
3066 my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3067 $sth->execute( shift );
3068 return "Deleted.";
3071 sub ProcessOfflineOperation {
3072 my $operation = shift;
3074 my $report;
3075 if ( $operation->{action} eq 'return' ) {
3076 $report = ProcessOfflineReturn( $operation );
3077 } elsif ( $operation->{action} eq 'issue' ) {
3078 $report = ProcessOfflineIssue( $operation );
3081 DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3083 return $report;
3086 sub ProcessOfflineReturn {
3087 my $operation = shift;
3089 my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3091 if ( $itemnumber ) {
3092 my $issue = GetOpenIssue( $itemnumber );
3093 if ( $issue ) {
3094 MarkIssueReturned(
3095 $issue->{borrowernumber},
3096 $itemnumber,
3097 undef,
3098 $operation->{timestamp},
3100 ModItem(
3101 { renewals => 0, onloan => undef },
3102 $issue->{'biblionumber'},
3103 $itemnumber
3105 return "Success.";
3106 } else {
3107 return "Item not issued.";
3109 } else {
3110 return "Item not found.";
3114 sub ProcessOfflineIssue {
3115 my $operation = shift;
3117 my $borrower = C4::Members::GetMemberDetails( undef, $operation->{cardnumber} ); # Get borrower from operation cardnumber
3119 if ( $borrower->{borrowernumber} ) {
3120 my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3121 unless ($itemnumber) {
3122 return "Barcode not found.";
3124 my $issue = GetOpenIssue( $itemnumber );
3126 if ( $issue and ( $issue->{borrowernumber} ne $borrower->{borrowernumber} ) ) { # Item already issued to another borrower, mark it returned
3127 MarkIssueReturned(
3128 $issue->{borrowernumber},
3129 $itemnumber,
3130 undef,
3131 $operation->{timestamp},
3134 AddIssue(
3135 $borrower,
3136 $operation->{'barcode'},
3137 undef,
3139 $operation->{timestamp},
3140 undef,
3142 return "Success.";
3143 } else {
3144 return "Borrower not found.";
3152 __END__
3154 =head1 AUTHOR
3156 Koha Development Team <http://koha-community.org/>
3158 =cut