Bug 9552 - BIB1 Relation "Greater Than" Attribute Not Mapped Properly in CCL.Properties
[koha.git] / C4 / Circulation.pm
blob063a409b62df03f8f97786a48f39f1257fecbac1
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 DateTime;
25 use C4::Context;
26 use C4::Stats;
27 use C4::Reserves;
28 use C4::Biblio;
29 use C4::Items;
30 use C4::Members;
31 use C4::Dates;
32 use C4::Dates qw(format_date);
33 use C4::Accounts;
34 use C4::ItemCirculationAlertPreference;
35 use C4::Message;
36 use C4::Debug;
37 use C4::Branch; # GetBranches
38 use C4::Log; # logaction
39 use C4::Koha qw(GetAuthorisedValueByCode);
40 use C4::Overdues qw(CalcFine UpdateFine);
41 use Algorithm::CheckDigits;
43 use Data::Dumper;
44 use Koha::DateUtils;
45 use Koha::Calendar;
46 use Carp;
47 use Date::Calc qw(
48 Today
49 Today_and_Now
50 Add_Delta_YM
51 Add_Delta_DHMS
52 Date_to_Days
53 Day_of_Week
54 Add_Delta_Days
56 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
58 BEGIN {
59 require Exporter;
60 $VERSION = 3.07.00.049; # for version checking
61 @ISA = qw(Exporter);
63 # FIXME subs that should probably be elsewhere
64 push @EXPORT, qw(
65 &barcodedecode
66 &LostItem
67 &ReturnLostItem
70 # subs to deal with issuing a book
71 push @EXPORT, qw(
72 &CanBookBeIssued
73 &CanBookBeRenewed
74 &AddIssue
75 &AddRenewal
76 &GetRenewCount
77 &GetItemIssue
78 &GetItemIssues
79 &GetIssuingCharges
80 &GetIssuingRule
81 &GetBranchBorrowerCircRule
82 &GetBranchItemRule
83 &GetBiblioIssues
84 &GetOpenIssue
85 &AnonymiseIssueHistory
86 &CheckIfIssuedToPatron
89 # subs to deal with returns
90 push @EXPORT, qw(
91 &AddReturn
92 &MarkIssueReturned
95 # subs to deal with transfers
96 push @EXPORT, qw(
97 &transferbook
98 &GetTransfers
99 &GetTransfersFromTo
100 &updateWrongTransfer
101 &DeleteTransfer
102 &IsBranchTransferAllowed
103 &CreateBranchTransferLimit
104 &DeleteBranchTransferLimits
105 &TransferSlip
108 # subs to deal with offline circulation
109 push @EXPORT, qw(
110 &GetOfflineOperations
111 &GetOfflineOperation
112 &AddOfflineOperation
113 &DeleteOfflineOperation
114 &ProcessOfflineOperation
118 =head1 NAME
120 C4::Circulation - Koha circulation module
122 =head1 SYNOPSIS
124 use C4::Circulation;
126 =head1 DESCRIPTION
128 The functions in this module deal with circulation, issues, and
129 returns, as well as general information about the library.
130 Also deals with stocktaking.
132 =head1 FUNCTIONS
134 =head2 barcodedecode
136 $str = &barcodedecode($barcode, [$filter]);
138 Generic filter function for barcode string.
139 Called on every circ if the System Pref itemBarcodeInputFilter is set.
140 Will do some manipulation of the barcode for systems that deliver a barcode
141 to circulation.pl that differs from the barcode stored for the item.
142 For proper functioning of this filter, calling the function on the
143 correct barcode string (items.barcode) should return an unaltered barcode.
145 The optional $filter argument is to allow for testing or explicit
146 behavior that ignores the System Pref. Valid values are the same as the
147 System Pref options.
149 =cut
151 # FIXME -- the &decode fcn below should be wrapped into this one.
152 # FIXME -- these plugins should be moved out of Circulation.pm
154 sub barcodedecode {
155 my ($barcode, $filter) = @_;
156 my $branch = C4::Branch::mybranch();
157 $filter = C4::Context->preference('itemBarcodeInputFilter') unless $filter;
158 $filter or return $barcode; # ensure filter is defined, else return untouched barcode
159 if ($filter eq 'whitespace') {
160 $barcode =~ s/\s//g;
161 } elsif ($filter eq 'cuecat') {
162 chomp($barcode);
163 my @fields = split( /\./, $barcode );
164 my @results = map( decode($_), @fields[ 1 .. $#fields ] );
165 ($#results == 2) and return $results[2];
166 } elsif ($filter eq 'T-prefix') {
167 if ($barcode =~ /^[Tt](\d)/) {
168 (defined($1) and $1 eq '0') and return $barcode;
169 $barcode = substr($barcode, 2) + 0; # FIXME: probably should be substr($barcode, 1)
171 return sprintf("T%07d", $barcode);
172 # FIXME: $barcode could be "T1", causing warning: substr outside of string
173 # Why drop the nonzero digit after the T?
174 # Why pass non-digits (or empty string) to "T%07d"?
175 } elsif ($filter eq 'libsuite8') {
176 unless($barcode =~ m/^($branch)-/i){ #if barcode starts with branch code its in Koha style. Skip it.
177 if($barcode =~ m/^(\d)/i){ #Some barcodes even start with 0's & numbers and are assumed to have b as the item type in the libsuite8 software
178 $barcode =~ s/^[0]*(\d+)$/$branch-b-$1/i;
179 }else{
180 $barcode =~ s/^(\D+)[0]*(\d+)$/$branch-$1-$2/i;
183 } elsif ($filter eq 'EAN13') {
184 my $ean = CheckDigits('ean');
185 if ( $ean->is_valid($barcode) ) {
186 #$barcode = sprintf('%013d',$barcode); # this doesn't work on 32-bit systems
187 $barcode = '0' x ( 13 - length($barcode) ) . $barcode;
188 } else {
189 warn "# [$barcode] not valid EAN-13/UPC-A\n";
192 return $barcode; # return barcode, modified or not
195 =head2 decode
197 $str = &decode($chunk);
199 Decodes a segment of a string emitted by a CueCat barcode scanner and
200 returns it.
202 FIXME: Should be replaced with Barcode::Cuecat from CPAN
203 or Javascript based decoding on the client side.
205 =cut
207 sub decode {
208 my ($encoded) = @_;
209 my $seq =
210 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-';
211 my @s = map { index( $seq, $_ ); } split( //, $encoded );
212 my $l = ( $#s + 1 ) % 4;
213 if ($l) {
214 if ( $l == 1 ) {
215 # warn "Error: Cuecat decode parsing failed!";
216 return;
218 $l = 4 - $l;
219 $#s += $l;
221 my $r = '';
222 while ( $#s >= 0 ) {
223 my $n = ( ( $s[0] << 6 | $s[1] ) << 6 | $s[2] ) << 6 | $s[3];
224 $r .=
225 chr( ( $n >> 16 ) ^ 67 )
226 .chr( ( $n >> 8 & 255 ) ^ 67 )
227 .chr( ( $n & 255 ) ^ 67 );
228 @s = @s[ 4 .. $#s ];
230 $r = substr( $r, 0, length($r) - $l );
231 return $r;
234 =head2 transferbook
236 ($dotransfer, $messages, $iteminformation) = &transferbook($newbranch,
237 $barcode, $ignore_reserves);
239 Transfers an item to a new branch. If the item is currently on loan, it is automatically returned before the actual transfer.
241 C<$newbranch> is the code for the branch to which the item should be transferred.
243 C<$barcode> is the barcode of the item to be transferred.
245 If C<$ignore_reserves> is true, C<&transferbook> ignores reserves.
246 Otherwise, if an item is reserved, the transfer fails.
248 Returns three values:
250 =over
252 =item $dotransfer
254 is true if the transfer was successful.
256 =item $messages
258 is a reference-to-hash which may have any of the following keys:
260 =over
262 =item C<BadBarcode>
264 There is no item in the catalog with the given barcode. The value is C<$barcode>.
266 =item C<IsPermanent>
268 The item's home branch is permanent. This doesn't prevent the item from being transferred, though. The value is the code of the item's home branch.
270 =item C<DestinationEqualsHolding>
272 The item is already at the branch to which it is being transferred. The transfer is nonetheless considered to have failed. The value should be ignored.
274 =item C<WasReturned>
276 The item was on loan, and C<&transferbook> automatically returned it before transferring it. The value is the borrower number of the patron who had the item.
278 =item C<ResFound>
280 The item was reserved. The value is a reference-to-hash whose keys are fields from the reserves table of the Koha database, and C<biblioitemnumber>. It also has the key C<ResFound>, whose value is either C<Waiting> or C<Reserved>.
282 =item C<WasTransferred>
284 The item was eligible to be transferred. Barring problems communicating with the database, the transfer should indeed have succeeded. The value should be ignored.
286 =back
288 =back
290 =cut
292 sub transferbook {
293 my ( $tbr, $barcode, $ignoreRs ) = @_;
294 my $messages;
295 my $dotransfer = 1;
296 my $branches = GetBranches();
297 my $itemnumber = GetItemnumberFromBarcode( $barcode );
298 my $issue = GetItemIssue($itemnumber);
299 my $biblio = GetBiblioFromItemNumber($itemnumber);
301 # bad barcode..
302 if ( not $itemnumber ) {
303 $messages->{'BadBarcode'} = $barcode;
304 $dotransfer = 0;
307 # get branches of book...
308 my $hbr = $biblio->{'homebranch'};
309 my $fbr = $biblio->{'holdingbranch'};
311 # if using Branch Transfer Limits
312 if ( C4::Context->preference("UseBranchTransferLimits") == 1 ) {
313 if ( C4::Context->preference("item-level_itypes") && C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ) {
314 if ( ! IsBranchTransferAllowed( $tbr, $fbr, $biblio->{'itype'} ) ) {
315 $messages->{'NotAllowed'} = $tbr . "::" . $biblio->{'itype'};
316 $dotransfer = 0;
318 } elsif ( ! IsBranchTransferAllowed( $tbr, $fbr, $biblio->{ C4::Context->preference("BranchTransferLimitsType") } ) ) {
319 $messages->{'NotAllowed'} = $tbr . "::" . $biblio->{ C4::Context->preference("BranchTransferLimitsType") };
320 $dotransfer = 0;
324 # if is permanent...
325 if ( $hbr && $branches->{$hbr}->{'PE'} ) {
326 $messages->{'IsPermanent'} = $hbr;
327 $dotransfer = 0;
330 # can't transfer book if is already there....
331 if ( $fbr eq $tbr ) {
332 $messages->{'DestinationEqualsHolding'} = 1;
333 $dotransfer = 0;
336 # check if it is still issued to someone, return it...
337 if ($issue->{borrowernumber}) {
338 AddReturn( $barcode, $fbr );
339 $messages->{'WasReturned'} = $issue->{borrowernumber};
342 # find reserves.....
343 # That'll save a database query.
344 my ( $resfound, $resrec, undef ) =
345 CheckReserves( $itemnumber );
346 if ( $resfound and not $ignoreRs ) {
347 $resrec->{'ResFound'} = $resfound;
349 # $messages->{'ResFound'} = $resrec;
350 $dotransfer = 1;
353 #actually do the transfer....
354 if ($dotransfer) {
355 ModItemTransfer( $itemnumber, $fbr, $tbr );
357 # don't need to update MARC anymore, we do it in batch now
358 $messages->{'WasTransfered'} = 1;
361 ModDateLastSeen( $itemnumber );
362 return ( $dotransfer, $messages, $biblio );
366 sub TooMany {
367 my $borrower = shift;
368 my $biblionumber = shift;
369 my $item = shift;
370 my $cat_borrower = $borrower->{'categorycode'};
371 my $dbh = C4::Context->dbh;
372 my $branch;
373 # Get which branchcode we need
374 $branch = _GetCircControlBranch($item,$borrower);
375 my $type = (C4::Context->preference('item-level_itypes'))
376 ? $item->{'itype'} # item-level
377 : $item->{'itemtype'}; # biblio-level
379 # given branch, patron category, and item type, determine
380 # applicable issuing rule
381 my $issuing_rule = GetIssuingRule($cat_borrower, $type, $branch);
383 # if a rule is found and has a loan limit set, count
384 # how many loans the patron already has that meet that
385 # rule
386 if (defined($issuing_rule) and defined($issuing_rule->{'maxissueqty'})) {
387 my @bind_params;
388 my $count_query = "SELECT COUNT(*) FROM issues
389 JOIN items USING (itemnumber) ";
391 my $rule_itemtype = $issuing_rule->{itemtype};
392 if ($rule_itemtype eq "*") {
393 # matching rule has the default item type, so count only
394 # those existing loans that don't fall under a more
395 # specific rule
396 if (C4::Context->preference('item-level_itypes')) {
397 $count_query .= " WHERE items.itype NOT IN (
398 SELECT itemtype FROM issuingrules
399 WHERE branchcode = ?
400 AND (categorycode = ? OR categorycode = ?)
401 AND itemtype <> '*'
402 ) ";
403 } else {
404 $count_query .= " JOIN biblioitems USING (biblionumber)
405 WHERE biblioitems.itemtype NOT IN (
406 SELECT itemtype FROM issuingrules
407 WHERE branchcode = ?
408 AND (categorycode = ? OR categorycode = ?)
409 AND itemtype <> '*'
410 ) ";
412 push @bind_params, $issuing_rule->{branchcode};
413 push @bind_params, $issuing_rule->{categorycode};
414 push @bind_params, $cat_borrower;
415 } else {
416 # rule has specific item type, so count loans of that
417 # specific item type
418 if (C4::Context->preference('item-level_itypes')) {
419 $count_query .= " WHERE items.itype = ? ";
420 } else {
421 $count_query .= " JOIN biblioitems USING (biblionumber)
422 WHERE biblioitems.itemtype= ? ";
424 push @bind_params, $type;
427 $count_query .= " AND borrowernumber = ? ";
428 push @bind_params, $borrower->{'borrowernumber'};
429 my $rule_branch = $issuing_rule->{branchcode};
430 if ($rule_branch ne "*") {
431 if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
432 $count_query .= " AND issues.branchcode = ? ";
433 push @bind_params, $branch;
434 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
435 ; # if branch is the patron's home branch, then count all loans by patron
436 } else {
437 $count_query .= " AND items.homebranch = ? ";
438 push @bind_params, $branch;
442 my $count_sth = $dbh->prepare($count_query);
443 $count_sth->execute(@bind_params);
444 my ($current_loan_count) = $count_sth->fetchrow_array;
446 my $max_loans_allowed = $issuing_rule->{'maxissueqty'};
447 if ($current_loan_count >= $max_loans_allowed) {
448 return ($current_loan_count, $max_loans_allowed);
452 # Now count total loans against the limit for the branch
453 my $branch_borrower_circ_rule = GetBranchBorrowerCircRule($branch, $cat_borrower);
454 if (defined($branch_borrower_circ_rule->{maxissueqty})) {
455 my @bind_params = ();
456 my $branch_count_query = "SELECT COUNT(*) FROM issues
457 JOIN items USING (itemnumber)
458 WHERE borrowernumber = ? ";
459 push @bind_params, $borrower->{borrowernumber};
461 if (C4::Context->preference('CircControl') eq 'PickupLibrary') {
462 $branch_count_query .= " AND issues.branchcode = ? ";
463 push @bind_params, $branch;
464 } elsif (C4::Context->preference('CircControl') eq 'PatronLibrary') {
465 ; # if branch is the patron's home branch, then count all loans by patron
466 } else {
467 $branch_count_query .= " AND items.homebranch = ? ";
468 push @bind_params, $branch;
470 my $branch_count_sth = $dbh->prepare($branch_count_query);
471 $branch_count_sth->execute(@bind_params);
472 my ($current_loan_count) = $branch_count_sth->fetchrow_array;
474 my $max_loans_allowed = $branch_borrower_circ_rule->{maxissueqty};
475 if ($current_loan_count >= $max_loans_allowed) {
476 return ($current_loan_count, $max_loans_allowed);
480 # OK, the patron can issue !!!
481 return;
484 =head2 itemissues
486 @issues = &itemissues($biblioitemnumber, $biblio);
488 Looks up information about who has borrowed the bookZ<>(s) with the
489 given biblioitemnumber.
491 C<$biblio> is ignored.
493 C<&itemissues> returns an array of references-to-hash. The keys
494 include the fields from the C<items> table in the Koha database.
495 Additional keys include:
497 =over 4
499 =item C<date_due>
501 If the item is currently on loan, this gives the due date.
503 If the item is not on loan, then this is either "Available" or
504 "Cancelled", if the item has been withdrawn.
506 =item C<card>
508 If the item is currently on loan, this gives the card number of the
509 patron who currently has the item.
511 =item C<timestamp0>, C<timestamp1>, C<timestamp2>
513 These give the timestamp for the last three times the item was
514 borrowed.
516 =item C<card0>, C<card1>, C<card2>
518 The card number of the last three patrons who borrowed this item.
520 =item C<borrower0>, C<borrower1>, C<borrower2>
522 The borrower number of the last three patrons who borrowed this item.
524 =back
526 =cut
529 sub itemissues {
530 my ( $bibitem, $biblio ) = @_;
531 my $dbh = C4::Context->dbh;
532 my $sth =
533 $dbh->prepare("Select * from items where items.biblioitemnumber = ?")
534 || die $dbh->errstr;
535 my $i = 0;
536 my @results;
538 $sth->execute($bibitem) || die $sth->errstr;
540 while ( my $data = $sth->fetchrow_hashref ) {
542 # Find out who currently has this item.
543 # FIXME - Wouldn't it be better to do this as a left join of
544 # some sort? Currently, this code assumes that if
545 # fetchrow_hashref() fails, then the book is on the shelf.
546 # fetchrow_hashref() can fail for any number of reasons (e.g.,
547 # database server crash), not just because no items match the
548 # search criteria.
549 my $sth2 = $dbh->prepare(
550 "SELECT * FROM issues
551 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
552 WHERE itemnumber = ?
556 $sth2->execute( $data->{'itemnumber'} );
557 if ( my $data2 = $sth2->fetchrow_hashref ) {
558 $data->{'date_due'} = $data2->{'date_due'};
559 $data->{'card'} = $data2->{'cardnumber'};
560 $data->{'borrower'} = $data2->{'borrowernumber'};
562 else {
563 $data->{'date_due'} = ($data->{'wthdrawn'} eq '1') ? 'Cancelled' : 'Available';
567 # Find the last 3 people who borrowed this item.
568 $sth2 = $dbh->prepare(
569 "SELECT * FROM old_issues
570 LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber
571 WHERE itemnumber = ?
572 ORDER BY returndate DESC,timestamp DESC"
575 $sth2->execute( $data->{'itemnumber'} );
576 for ( my $i2 = 0 ; $i2 < 2 ; $i2++ )
577 { # FIXME : error if there is less than 3 pple borrowing this item
578 if ( my $data2 = $sth2->fetchrow_hashref ) {
579 $data->{"timestamp$i2"} = $data2->{'timestamp'};
580 $data->{"card$i2"} = $data2->{'cardnumber'};
581 $data->{"borrower$i2"} = $data2->{'borrowernumber'};
582 } # if
583 } # for
585 $results[$i] = $data;
586 $i++;
589 return (@results);
592 =head2 CanBookBeIssued
594 ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $borrower,
595 $barcode, $duedatespec, $inprocess, $ignore_reserves );
597 Check if a book can be issued.
599 C<$issuingimpossible> and C<$needsconfirmation> are some hashref.
601 =over 4
603 =item C<$borrower> hash with borrower informations (from GetMember or GetMemberDetails)
605 =item C<$barcode> is the bar code of the book being issued.
607 =item C<$duedatespec> is a C4::Dates object.
609 =item C<$inprocess> boolean switch
610 =item C<$ignore_reserves> boolean switch
612 =back
614 Returns :
616 =over 4
618 =item C<$issuingimpossible> a reference to a hash. It contains reasons why issuing is impossible.
619 Possible values are :
621 =back
623 =head3 INVALID_DATE
625 sticky due date is invalid
627 =head3 GNA
629 borrower gone with no address
631 =head3 CARD_LOST
633 borrower declared it's card lost
635 =head3 DEBARRED
637 borrower debarred
639 =head3 UNKNOWN_BARCODE
641 barcode unknown
643 =head3 NOT_FOR_LOAN
645 item is not for loan
647 =head3 WTHDRAWN
649 item withdrawn.
651 =head3 RESTRICTED
653 item is restricted (set by ??)
655 C<$needsconfirmation> a reference to a hash. It contains reasons why the loan
656 could be prevented, but ones that can be overriden by the operator.
658 Possible values are :
660 =head3 DEBT
662 borrower has debts.
664 =head3 RENEW_ISSUE
666 renewing, not issuing
668 =head3 ISSUED_TO_ANOTHER
670 issued to someone else.
672 =head3 RESERVED
674 reserved for someone else.
676 =head3 INVALID_DATE
678 sticky due date is invalid or due date in the past
680 =head3 TOO_MANY
682 if the borrower borrows to much things
684 =cut
686 sub CanBookBeIssued {
687 my ( $borrower, $barcode, $duedate, $inprocess, $ignore_reserves ) = @_;
688 my %needsconfirmation; # filled with problems that needs confirmations
689 my %issuingimpossible; # filled with problems that causes the issue to be IMPOSSIBLE
690 my %alerts; # filled with messages that shouldn't stop issuing, but the librarian should be aware of.
692 my $item = GetItem(GetItemnumberFromBarcode( $barcode ));
693 my $issue = GetItemIssue($item->{itemnumber});
694 my $biblioitem = GetBiblioItemData($item->{biblioitemnumber});
695 $item->{'itemtype'}=$item->{'itype'};
696 my $dbh = C4::Context->dbh;
698 # MANDATORY CHECKS - unless item exists, nothing else matters
699 unless ( $item->{barcode} ) {
700 $issuingimpossible{UNKNOWN_BARCODE} = 1;
702 return ( \%issuingimpossible, \%needsconfirmation ) if %issuingimpossible;
705 # DUE DATE is OK ? -- should already have checked.
707 if ($duedate && ref $duedate ne 'DateTime') {
708 $duedate = dt_from_string($duedate);
710 my $now = DateTime->now( time_zone => C4::Context->tz() );
711 unless ( $duedate ) {
712 my $issuedate = $now->clone();
714 my $branch = _GetCircControlBranch($item,$borrower);
715 my $itype = ( C4::Context->preference('item-level_itypes') ) ? $item->{'itype'} : $biblioitem->{'itemtype'};
716 $duedate = CalcDateDue( $issuedate, $itype, $branch, $borrower );
718 # Offline circ calls AddIssue directly, doesn't run through here
719 # So issuingimpossible should be ok.
721 if ($duedate) {
722 my $today = $now->clone();
723 $today->truncate( to => 'minute');
724 if (DateTime->compare($duedate,$today) == -1 ) { # duedate cannot be before now
725 $needsconfirmation{INVALID_DATE} = output_pref($duedate);
727 } else {
728 $issuingimpossible{INVALID_DATE} = output_pref($duedate);
732 # BORROWER STATUS
734 if ( $borrower->{'category_type'} eq 'X' && ( $item->{barcode} )) {
735 # stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1 .
736 &UpdateStats(C4::Context->userenv->{'branch'},'localuse','','',$item->{'itemnumber'},$item->{'itemtype'},$borrower->{'borrowernumber'}, undef, $item->{'ccode'});
737 ModDateLastSeen( $item->{'itemnumber'} );
738 return( { STATS => 1 }, {});
740 if ( $borrower->{flags}->{GNA} ) {
741 $issuingimpossible{GNA} = 1;
743 if ( $borrower->{flags}->{'LOST'} ) {
744 $issuingimpossible{CARD_LOST} = 1;
746 if ( $borrower->{flags}->{'DBARRED'} ) {
747 $issuingimpossible{DEBARRED} = 1;
749 if ( !defined $borrower->{dateexpiry} || $borrower->{'dateexpiry'} eq '0000-00-00') {
750 $issuingimpossible{EXPIRED} = 1;
751 } else {
752 my ($y, $m, $d) = split /-/,$borrower->{'dateexpiry'};
753 if ($y && $m && $d) { # are we really writing oinvalid dates to borrs
754 my $expiry_dt = DateTime->new(
755 year => $y,
756 month => $m,
757 day => $d,
758 time_zone => C4::Context->tz,
760 $expiry_dt->truncate( to => 'day');
761 my $today = $now->clone()->truncate(to => 'day');
762 if (DateTime->compare($today, $expiry_dt) == 1) {
763 $issuingimpossible{EXPIRED} = 1;
765 } else {
766 carp("Invalid expity date in borr");
767 $issuingimpossible{EXPIRED} = 1;
771 # BORROWER STATUS
774 # DEBTS
775 my ($balance, $non_issue_charges, $other_charges) =
776 C4::Members::GetMemberAccountBalance( $borrower->{'borrowernumber'} );
777 my $amountlimit = C4::Context->preference("noissuescharge");
778 my $allowfineoverride = C4::Context->preference("AllowFineOverride");
779 my $allfinesneedoverride = C4::Context->preference("AllFinesNeedOverride");
780 if ( C4::Context->preference("IssuingInProcess") ) {
781 if ( $non_issue_charges > $amountlimit && !$inprocess && !$allowfineoverride) {
782 $issuingimpossible{DEBT} = sprintf( "%.2f", $non_issue_charges );
783 } elsif ( $non_issue_charges > $amountlimit && !$inprocess && $allowfineoverride) {
784 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
785 } elsif ( $allfinesneedoverride && $non_issue_charges > 0 && $non_issue_charges <= $amountlimit && !$inprocess ) {
786 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
789 else {
790 if ( $non_issue_charges > $amountlimit && $allowfineoverride ) {
791 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
792 } elsif ( $non_issue_charges > $amountlimit && !$allowfineoverride) {
793 $issuingimpossible{DEBT} = sprintf( "%.2f", $non_issue_charges );
794 } elsif ( $non_issue_charges > 0 && $allfinesneedoverride ) {
795 $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges );
798 if ($balance > 0 && $other_charges > 0) {
799 $alerts{OTHER_CHARGES} = sprintf( "%.2f", $other_charges );
802 my ($blocktype, $count) = C4::Members::IsMemberBlocked($borrower->{'borrowernumber'});
803 if ($blocktype == -1) {
804 ## patron has outstanding overdue loans
805 if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){
806 $issuingimpossible{USERBLOCKEDOVERDUE} = $count;
808 elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){
809 $needsconfirmation{USERBLOCKEDOVERDUE} = $count;
811 } elsif($blocktype == 1) {
812 # patron has accrued fine days
813 $issuingimpossible{USERBLOCKEDREMAINING} = $count;
817 # JB34 CHECKS IF BORROWERS DONT HAVE ISSUE TOO MANY BOOKS
819 my ($current_loan_count, $max_loans_allowed) = TooMany( $borrower, $item->{biblionumber}, $item );
820 # if TooMany max_loans_allowed returns 0 the user doesn't have permission to check out this book
821 if (defined $max_loans_allowed && $max_loans_allowed == 0) {
822 $needsconfirmation{PATRON_CANT} = 1;
823 } else {
824 if($max_loans_allowed){
825 $needsconfirmation{TOO_MANY} = 1;
826 $needsconfirmation{current_loan_count} = $current_loan_count;
827 $needsconfirmation{max_loans_allowed} = $max_loans_allowed;
832 # ITEM CHECKING
834 if ( $item->{'notforloan'}
835 && $item->{'notforloan'} > 0 )
837 if(!C4::Context->preference("AllowNotForLoanOverride")){
838 $issuingimpossible{NOT_FOR_LOAN} = 1;
839 }else{
840 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
843 elsif ( !$item->{'notforloan'} ){
844 # we have to check itemtypes.notforloan also
845 if (C4::Context->preference('item-level_itypes')){
846 # this should probably be a subroutine
847 my $sth = $dbh->prepare("SELECT notforloan FROM itemtypes WHERE itemtype = ?");
848 $sth->execute($item->{'itemtype'});
849 my $notforloan=$sth->fetchrow_hashref();
850 $sth->finish();
851 if ($notforloan->{'notforloan'}) {
852 if (!C4::Context->preference("AllowNotForLoanOverride")) {
853 $issuingimpossible{NOT_FOR_LOAN} = 1;
854 } else {
855 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
859 elsif ($biblioitem->{'notforloan'} == 1){
860 if (!C4::Context->preference("AllowNotForLoanOverride")) {
861 $issuingimpossible{NOT_FOR_LOAN} = 1;
862 } else {
863 $needsconfirmation{NOT_FOR_LOAN_FORCING} = 1;
867 if ( $item->{'wthdrawn'} && $item->{'wthdrawn'} > 0 )
869 $issuingimpossible{WTHDRAWN} = 1;
871 if ( $item->{'restricted'}
872 && $item->{'restricted'} == 1 )
874 $issuingimpossible{RESTRICTED} = 1;
876 if ( $item->{'itemlost'} && C4::Context->preference("IssueLostItem") ne 'nothing' ) {
877 my $code = GetAuthorisedValueByCode( 'LOST', $item->{'itemlost'} );
878 $needsconfirmation{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'confirm' );
879 $alerts{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'alert' );
881 if ( C4::Context->preference("IndependantBranches") ) {
882 my $userenv = C4::Context->userenv;
883 if ( ($userenv) && ( $userenv->{flags} % 2 != 1 ) ) {
884 $issuingimpossible{ITEMNOTSAMEBRANCH} = 1
885 if ( $item->{C4::Context->preference("HomeOrHoldingBranch")} ne $userenv->{branch} );
886 $needsconfirmation{BORRNOTSAMEBRANCH} = GetBranchName( $borrower->{'branchcode'} )
887 if ( $borrower->{'branchcode'} ne $userenv->{branch} );
892 # CHECK IF BOOK ALREADY ISSUED TO THIS BORROWER
894 if ( $issue->{borrowernumber} && $issue->{borrowernumber} eq $borrower->{'borrowernumber'} )
897 # Already issued to current borrower. Ask whether the loan should
898 # be renewed.
899 my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed(
900 $borrower->{'borrowernumber'},
901 $item->{'itemnumber'}
903 if ( $CanBookBeRenewed == 0 ) { # no more renewals allowed
904 $issuingimpossible{NO_MORE_RENEWALS} = 1;
906 else {
907 $needsconfirmation{RENEW_ISSUE} = 1;
910 elsif ($issue->{borrowernumber}) {
912 # issued to someone else
913 my $currborinfo = C4::Members::GetMember( borrowernumber => $issue->{borrowernumber} );
915 # warn "=>.$currborinfo->{'firstname'} $currborinfo->{'surname'} ($currborinfo->{'cardnumber'})";
916 $needsconfirmation{ISSUED_TO_ANOTHER} = 1;
917 $needsconfirmation{issued_firstname} = $currborinfo->{'firstname'};
918 $needsconfirmation{issued_surname} = $currborinfo->{'surname'};
919 $needsconfirmation{issued_cardnumber} = $currborinfo->{'cardnumber'};
920 $needsconfirmation{issued_borrowernumber} = $currborinfo->{'borrowernumber'};
923 unless ( $ignore_reserves ) {
924 # See if the item is on reserve.
925 my ( $restype, $res ) = C4::Reserves::CheckReserves( $item->{'itemnumber'} );
926 if ($restype) {
927 my $resbor = $res->{'borrowernumber'};
928 if ( $resbor ne $borrower->{'borrowernumber'} ) {
929 my ( $resborrower ) = C4::Members::GetMember( borrowernumber => $resbor );
930 my $branchname = GetBranchName( $res->{'branchcode'} );
931 if ( $restype eq "Waiting" )
933 # The item is on reserve and waiting, but has been
934 # reserved by some other patron.
935 $needsconfirmation{RESERVE_WAITING} = 1;
936 $needsconfirmation{'resfirstname'} = $resborrower->{'firstname'};
937 $needsconfirmation{'ressurname'} = $resborrower->{'surname'};
938 $needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'};
939 $needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'};
940 $needsconfirmation{'resbranchname'} = $branchname;
941 $needsconfirmation{'reswaitingdate'} = format_date($res->{'waitingdate'});
943 elsif ( $restype eq "Reserved" ) {
944 # The item is on reserve for someone else.
945 $needsconfirmation{RESERVED} = 1;
946 $needsconfirmation{'resfirstname'} = $resborrower->{'firstname'};
947 $needsconfirmation{'ressurname'} = $resborrower->{'surname'};
948 $needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'};
949 $needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'};
950 $needsconfirmation{'resbranchname'} = $branchname;
951 $needsconfirmation{'resreservedate'} = format_date($res->{'reservedate'});
957 # CHECK AGE RESTRICTION
960 # get $marker from preferences. Could be something like "FSK|PEGI|Alter|Age:"
961 my $markers = C4::Context->preference('AgeRestrictionMarker' );
962 my $bibvalues = $biblioitem->{'agerestriction'};
963 if (($markers)&&($bibvalues))
965 # Split $bibvalues to something like FSK 16 or PEGI 6
966 my @values = split ' ', $bibvalues;
968 # Search first occurence of one of the markers
969 my @markers = split /\|/, $markers;
970 my $index = 0;
971 my $take = -1;
972 for my $value (@values) {
973 $index ++;
974 for my $marker (@markers) {
975 $marker =~ s/^\s+//; #remove leading spaces
976 $marker =~ s/\s+$//; #remove trailing spaces
977 if (uc($marker) eq uc($value)) {
978 $take = $index;
979 last;
982 if ($take > -1) {
983 last;
986 # Index points to the next value
987 my $restrictionyear = 0;
988 if (($take <= $#values) && ($take >= 0)){
989 $restrictionyear += $values[$take];
992 if ($restrictionyear > 0) {
993 if ( $borrower->{'dateofbirth'} ) {
994 my @alloweddate = split /-/,$borrower->{'dateofbirth'} ;
995 $alloweddate[0] += $restrictionyear;
996 #Prevent runime eror on leap year (invalid date)
997 if (($alloweddate[1] == 2) && ($alloweddate[2] == 29)) {
998 $alloweddate[2] = 28;
1001 if ( Date_to_Days(Today) < Date_to_Days(@alloweddate) -1 ) {
1002 if (C4::Context->preference('AgeRestrictionOverride' )) {
1003 $needsconfirmation{AGE_RESTRICTION} = "$bibvalues";
1005 else {
1006 $issuingimpossible{AGE_RESTRICTION} = "$bibvalues";
1013 ## check for high holds decreasing loan period
1014 my $decrease_loan = C4::Context->preference('decreaseLoanHighHolds');
1015 if ( $decrease_loan && $decrease_loan == 1 ) {
1016 my ( $reserved, $num, $duration, $returndate ) =
1017 checkHighHolds( $item, $borrower );
1019 if ( $num >= C4::Context->preference('decreaseLoanHighHoldsValue') ) {
1020 $needsconfirmation{HIGHHOLDS} = {
1021 num_holds => $num,
1022 duration => $duration,
1023 returndate => output_pref($returndate),
1028 return ( \%issuingimpossible, \%needsconfirmation, \%alerts );
1031 =head2 CanBookBeReturned
1033 ($returnallowed, $message) = CanBookBeReturned($item, $branch)
1035 Check whether the item can be returned to the provided branch
1037 =over 4
1039 =item C<$item> is a hash of item information as returned from GetItem
1041 =item C<$branch> is the branchcode where the return is taking place
1043 =back
1045 Returns:
1047 =over 4
1049 =item C<$returnallowed> is 0 or 1, corresponding to whether the return is allowed (1) or not (0)
1051 =item C<$message> is the branchcode where the item SHOULD be returned, if the return is not allowed
1053 =back
1055 =cut
1057 sub CanBookBeReturned {
1058 my ($item, $branch) = @_;
1059 my $allowreturntobranch = C4::Context->preference("AllowReturnToBranch") || 'anywhere';
1061 # assume return is allowed to start
1062 my $allowed = 1;
1063 my $message;
1065 # identify all cases where return is forbidden
1066 if ($allowreturntobranch eq 'homebranch' && $branch ne $item->{'homebranch'}) {
1067 $allowed = 0;
1068 $message = $item->{'homebranch'};
1069 } elsif ($allowreturntobranch eq 'holdingbranch' && $branch ne $item->{'holdingbranch'}) {
1070 $allowed = 0;
1071 $message = $item->{'holdingbranch'};
1072 } elsif ($allowreturntobranch eq 'homeorholdingbranch' && $branch ne $item->{'homebranch'} && $branch ne $item->{'holdingbranch'}) {
1073 $allowed = 0;
1074 $message = $item->{'homebranch'}; # FIXME: choice of homebranch is arbitrary
1077 return ($allowed, $message);
1080 =head2 CheckHighHolds
1082 used when syspref decreaseLoanHighHolds is active. Returns 1 or 0 to define whether the minimum value held in
1083 decreaseLoanHighHoldsValue is exceeded, the total number of outstanding holds, the number of days the loan
1084 has been decreased to (held in syspref decreaseLoanHighHoldsValue), and the new due date
1086 =cut
1088 sub checkHighHolds {
1089 my ( $item, $borrower ) = @_;
1090 my $biblio = GetBiblioFromItemNumber( $item->{itemnumber} );
1091 my $branch = _GetCircControlBranch( $item, $borrower );
1092 my $dbh = C4::Context->dbh;
1093 my $sth = $dbh->prepare(
1094 'select count(borrowernumber) as num_holds from reserves where biblionumber=?'
1096 $sth->execute( $item->{'biblionumber'} );
1097 my ($holds) = $sth->fetchrow_array;
1098 if ($holds) {
1099 my $issuedate = DateTime->now( time_zone => C4::Context->tz() );
1101 my $calendar = Koha::Calendar->new( branchcode => $branch );
1103 my $itype =
1104 ( C4::Context->preference('item-level_itypes') )
1105 ? $biblio->{'itype'}
1106 : $biblio->{'itemtype'};
1107 my $orig_due =
1108 C4::Circulation::CalcDateDue( $issuedate, $itype, $branch,
1109 $borrower );
1111 my $reduced_datedue =
1112 $calendar->addDate( $issuedate,
1113 C4::Context->preference('decreaseLoanHighHoldsDuration') );
1115 if ( DateTime->compare( $reduced_datedue, $orig_due ) == -1 ) {
1116 return ( 1, $holds,
1117 C4::Context->preference('decreaseLoanHighHoldsDuration'),
1118 $reduced_datedue );
1121 return ( 0, 0, 0, undef );
1124 =head2 AddIssue
1126 &AddIssue($borrower, $barcode, [$datedue], [$cancelreserve], [$issuedate])
1128 Issue a book. Does no check, they are done in CanBookBeIssued. If we reach this sub, it means the user confirmed if needed.
1130 =over 4
1132 =item C<$borrower> is a hash with borrower informations (from GetMember or GetMemberDetails).
1134 =item C<$barcode> is the barcode of the item being issued.
1136 =item C<$datedue> is a C4::Dates object for the max date of return, i.e. the date due (optional).
1137 Calculated if empty.
1139 =item C<$cancelreserve> is 1 to override and cancel any pending reserves for the item (optional).
1141 =item C<$issuedate> is the date to issue the item in iso (YYYY-MM-DD) format (optional).
1142 Defaults to today. Unlike C<$datedue>, NOT a C4::Dates object, unfortunately.
1144 AddIssue does the following things :
1146 - step 01: check that there is a borrowernumber & a barcode provided
1147 - check for RENEWAL (book issued & being issued to the same patron)
1148 - renewal YES = Calculate Charge & renew
1149 - renewal NO =
1150 * BOOK ACTUALLY ISSUED ? do a return if book is actually issued (but to someone else)
1151 * RESERVE PLACED ?
1152 - fill reserve if reserve to this patron
1153 - cancel reserve or not, otherwise
1154 * TRANSFERT PENDING ?
1155 - complete the transfert
1156 * ISSUE THE BOOK
1158 =back
1160 =cut
1162 sub AddIssue {
1163 my ( $borrower, $barcode, $datedue, $cancelreserve, $issuedate, $sipmode) = @_;
1164 my $dbh = C4::Context->dbh;
1165 my $barcodecheck=CheckValidBarcode($barcode);
1166 if ($datedue && ref $datedue ne 'DateTime') {
1167 $datedue = dt_from_string($datedue);
1169 # $issuedate defaults to today.
1170 if ( ! defined $issuedate ) {
1171 $issuedate = DateTime->now(time_zone => C4::Context->tz());
1173 else {
1174 if ( ref $issuedate ne 'DateTime') {
1175 $issuedate = dt_from_string($issuedate);
1179 if ($borrower and $barcode and $barcodecheck ne '0'){#??? wtf
1180 # find which item we issue
1181 my $item = GetItem('', $barcode) or return; # if we don't get an Item, abort.
1182 my $branch = _GetCircControlBranch($item,$borrower);
1184 # get actual issuing if there is one
1185 my $actualissue = GetItemIssue( $item->{itemnumber});
1187 # get biblioinformation for this item
1188 my $biblio = GetBiblioFromItemNumber($item->{itemnumber});
1191 # check if we just renew the issue.
1193 if ($actualissue->{borrowernumber} eq $borrower->{'borrowernumber'}) {
1194 $datedue = AddRenewal(
1195 $borrower->{'borrowernumber'},
1196 $item->{'itemnumber'},
1197 $branch,
1198 $datedue,
1199 $issuedate, # here interpreted as the renewal date
1202 else {
1203 # it's NOT a renewal
1204 if ( $actualissue->{borrowernumber}) {
1205 # This book is currently on loan, but not to the person
1206 # who wants to borrow it now. mark it returned before issuing to the new borrower
1207 AddReturn(
1208 $item->{'barcode'},
1209 C4::Context->userenv->{'branch'}
1213 MoveReserve( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $cancelreserve );
1214 # Starting process for transfer job (checking transfert and validate it if we have one)
1215 my ($datesent) = GetTransfers($item->{'itemnumber'});
1216 if ($datesent) {
1217 # updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....)
1218 my $sth =
1219 $dbh->prepare(
1220 "UPDATE branchtransfers
1221 SET datearrived = now(),
1222 tobranch = ?,
1223 comments = 'Forced branchtransfer'
1224 WHERE itemnumber= ? AND datearrived IS NULL"
1226 $sth->execute(C4::Context->userenv->{'branch'},$item->{'itemnumber'});
1229 # Record in the database the fact that the book was issued.
1230 my $sth =
1231 $dbh->prepare(
1232 "INSERT INTO issues
1233 (borrowernumber, itemnumber,issuedate, date_due, branchcode)
1234 VALUES (?,?,?,?,?)"
1236 unless ($datedue) {
1237 my $itype = ( C4::Context->preference('item-level_itypes') ) ? $biblio->{'itype'} : $biblio->{'itemtype'};
1238 $datedue = CalcDateDue( $issuedate, $itype, $branch, $borrower );
1241 $datedue->truncate( to => 'minute');
1242 $sth->execute(
1243 $borrower->{'borrowernumber'}, # borrowernumber
1244 $item->{'itemnumber'}, # itemnumber
1245 $issuedate->strftime('%Y-%m-%d %H:%M:00'), # issuedate
1246 $datedue->strftime('%Y-%m-%d %H:%M:00'), # date_due
1247 C4::Context->userenv->{'branch'} # branchcode
1249 if ( C4::Context->preference('ReturnToShelvingCart') ) { ## ReturnToShelvingCart is on, anything issued should be taken off the cart.
1250 CartToShelf( $item->{'itemnumber'} );
1252 $item->{'issues'}++;
1253 if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) {
1254 UpdateTotalIssues($item->{'biblionumber'}, 1);
1257 ## If item was lost, it has now been found, reverse any list item charges if neccessary.
1258 if ( $item->{'itemlost'} ) {
1259 if ( C4::Context->preference('RefundLostItemFeeOnReturn' ) ) {
1260 _FixAccountForLostAndReturned( $item->{'itemnumber'}, undef, $item->{'barcode'} );
1264 ModItem({ issues => $item->{'issues'},
1265 holdingbranch => C4::Context->userenv->{'branch'},
1266 itemlost => 0,
1267 datelastborrowed => DateTime->now(time_zone => C4::Context->tz())->ymd(),
1268 onloan => $datedue->ymd(),
1269 }, $item->{'biblionumber'}, $item->{'itemnumber'});
1270 ModDateLastSeen( $item->{'itemnumber'} );
1272 # If it costs to borrow this book, charge it to the patron's account.
1273 my ( $charge, $itemtype ) = GetIssuingCharges(
1274 $item->{'itemnumber'},
1275 $borrower->{'borrowernumber'}
1277 if ( $charge > 0 ) {
1278 AddIssuingCharge(
1279 $item->{'itemnumber'},
1280 $borrower->{'borrowernumber'}, $charge
1282 $item->{'charge'} = $charge;
1285 # Record the fact that this book was issued.
1286 &UpdateStats(
1287 C4::Context->userenv->{'branch'},
1288 'issue', $charge,
1289 ($sipmode ? "SIP-$sipmode" : ''), $item->{'itemnumber'},
1290 $item->{'itype'}, $borrower->{'borrowernumber'}, undef, $item->{'ccode'}
1293 # Send a checkout slip.
1294 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1295 my %conditions = (
1296 branchcode => $branch,
1297 categorycode => $borrower->{categorycode},
1298 item_type => $item->{itype},
1299 notification => 'CHECKOUT',
1301 if ($circulation_alert->is_enabled_for(\%conditions)) {
1302 SendCirculationAlert({
1303 type => 'CHECKOUT',
1304 item => $item,
1305 borrower => $borrower,
1306 branch => $branch,
1311 logaction("CIRCULATION", "ISSUE", $borrower->{'borrowernumber'}, $biblio->{'biblionumber'})
1312 if C4::Context->preference("IssueLog");
1314 return ($datedue); # not necessarily the same as when it came in!
1317 =head2 GetLoanLength
1319 my $loanlength = &GetLoanLength($borrowertype,$itemtype,branchcode)
1321 Get loan length for an itemtype, a borrower type and a branch
1323 =cut
1325 sub GetLoanLength {
1326 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1327 my $dbh = C4::Context->dbh;
1328 my $sth =
1329 $dbh->prepare(
1330 'select issuelength, lengthunit from issuingrules where categorycode=? and itemtype=? and branchcode=? and issuelength is not null'
1332 # warn "in get loan lenght $borrowertype $itemtype $branchcode ";
1333 # try to find issuelength & return the 1st available.
1334 # check with borrowertype, itemtype and branchcode, then without one of those parameters
1335 $sth->execute( $borrowertype, $itemtype, $branchcode );
1336 my $loanlength = $sth->fetchrow_hashref;
1337 return $loanlength
1338 if defined($loanlength) && $loanlength->{issuelength};
1340 $sth->execute( $borrowertype, '*', $branchcode );
1341 $loanlength = $sth->fetchrow_hashref;
1342 return $loanlength
1343 if defined($loanlength) && $loanlength->{issuelength};
1345 $sth->execute( '*', $itemtype, $branchcode );
1346 $loanlength = $sth->fetchrow_hashref;
1347 return $loanlength
1348 if defined($loanlength) && $loanlength->{issuelength};
1350 $sth->execute( '*', '*', $branchcode );
1351 $loanlength = $sth->fetchrow_hashref;
1352 return $loanlength
1353 if defined($loanlength) && $loanlength->{issuelength};
1355 $sth->execute( $borrowertype, $itemtype, '*' );
1356 $loanlength = $sth->fetchrow_hashref;
1357 return $loanlength
1358 if defined($loanlength) && $loanlength->{issuelength};
1360 $sth->execute( $borrowertype, '*', '*' );
1361 $loanlength = $sth->fetchrow_hashref;
1362 return $loanlength
1363 if defined($loanlength) && $loanlength->{issuelength};
1365 $sth->execute( '*', $itemtype, '*' );
1366 $loanlength = $sth->fetchrow_hashref;
1367 return $loanlength
1368 if defined($loanlength) && $loanlength->{issuelength};
1370 $sth->execute( '*', '*', '*' );
1371 $loanlength = $sth->fetchrow_hashref;
1372 return $loanlength
1373 if defined($loanlength) && $loanlength->{issuelength};
1375 # if no rule is set => 21 days (hardcoded)
1376 return {
1377 issuelength => 21,
1378 lengthunit => 'days',
1384 =head2 GetHardDueDate
1386 my ($hardduedate,$hardduedatecompare) = &GetHardDueDate($borrowertype,$itemtype,branchcode)
1388 Get the Hard Due Date and it's comparison for an itemtype, a borrower type and a branch
1390 =cut
1392 sub GetHardDueDate {
1393 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1395 my $rule = GetIssuingRule( $borrowertype, $itemtype, $branchcode );
1397 if ( defined( $rule ) ) {
1398 if ( $rule->{hardduedate} ) {
1399 return (dt_from_string($rule->{hardduedate}, 'iso'),$rule->{hardduedatecompare});
1400 } else {
1401 return (undef, undef);
1406 =head2 GetIssuingRule
1408 my $irule = &GetIssuingRule($borrowertype,$itemtype,branchcode)
1410 FIXME - This is a copy-paste of GetLoanLength
1411 as a stop-gap. Do not wish to change API for GetLoanLength
1412 this close to release, however, Overdues::GetIssuingRules is broken.
1414 Get the issuing rule for an itemtype, a borrower type and a branch
1415 Returns a hashref from the issuingrules table.
1417 =cut
1419 sub GetIssuingRule {
1420 my ( $borrowertype, $itemtype, $branchcode ) = @_;
1421 my $dbh = C4::Context->dbh;
1422 my $sth = $dbh->prepare( "select * from issuingrules where categorycode=? and itemtype=? and branchcode=? and issuelength is not null" );
1423 my $irule;
1425 $sth->execute( $borrowertype, $itemtype, $branchcode );
1426 $irule = $sth->fetchrow_hashref;
1427 return $irule if defined($irule) ;
1429 $sth->execute( $borrowertype, "*", $branchcode );
1430 $irule = $sth->fetchrow_hashref;
1431 return $irule if defined($irule) ;
1433 $sth->execute( "*", $itemtype, $branchcode );
1434 $irule = $sth->fetchrow_hashref;
1435 return $irule if defined($irule) ;
1437 $sth->execute( "*", "*", $branchcode );
1438 $irule = $sth->fetchrow_hashref;
1439 return $irule if defined($irule) ;
1441 $sth->execute( $borrowertype, $itemtype, "*" );
1442 $irule = $sth->fetchrow_hashref;
1443 return $irule if defined($irule) ;
1445 $sth->execute( $borrowertype, "*", "*" );
1446 $irule = $sth->fetchrow_hashref;
1447 return $irule if defined($irule) ;
1449 $sth->execute( "*", $itemtype, "*" );
1450 $irule = $sth->fetchrow_hashref;
1451 return $irule if defined($irule) ;
1453 $sth->execute( "*", "*", "*" );
1454 $irule = $sth->fetchrow_hashref;
1455 return $irule if defined($irule) ;
1457 # if no rule matches,
1458 return;
1461 =head2 GetBranchBorrowerCircRule
1463 my $branch_cat_rule = GetBranchBorrowerCircRule($branchcode, $categorycode);
1465 Retrieves circulation rule attributes that apply to the given
1466 branch and patron category, regardless of item type.
1467 The return value is a hashref containing the following key:
1469 maxissueqty - maximum number of loans that a
1470 patron of the given category can have at the given
1471 branch. If the value is undef, no limit.
1473 This will first check for a specific branch and
1474 category match from branch_borrower_circ_rules.
1476 If no rule is found, it will then check default_branch_circ_rules
1477 (same branch, default category). If no rule is found,
1478 it will then check default_borrower_circ_rules (default
1479 branch, same category), then failing that, default_circ_rules
1480 (default branch, default category).
1482 If no rule has been found in the database, it will default to
1483 the buillt in rule:
1485 maxissueqty - undef
1487 C<$branchcode> and C<$categorycode> should contain the
1488 literal branch code and patron category code, respectively - no
1489 wildcards.
1491 =cut
1493 sub GetBranchBorrowerCircRule {
1494 my $branchcode = shift;
1495 my $categorycode = shift;
1497 my $branch_cat_query = "SELECT maxissueqty
1498 FROM branch_borrower_circ_rules
1499 WHERE branchcode = ?
1500 AND categorycode = ?";
1501 my $dbh = C4::Context->dbh();
1502 my $sth = $dbh->prepare($branch_cat_query);
1503 $sth->execute($branchcode, $categorycode);
1504 my $result;
1505 if ($result = $sth->fetchrow_hashref()) {
1506 return $result;
1509 # try same branch, default borrower category
1510 my $branch_query = "SELECT maxissueqty
1511 FROM default_branch_circ_rules
1512 WHERE branchcode = ?";
1513 $sth = $dbh->prepare($branch_query);
1514 $sth->execute($branchcode);
1515 if ($result = $sth->fetchrow_hashref()) {
1516 return $result;
1519 # try default branch, same borrower category
1520 my $category_query = "SELECT maxissueqty
1521 FROM default_borrower_circ_rules
1522 WHERE categorycode = ?";
1523 $sth = $dbh->prepare($category_query);
1524 $sth->execute($categorycode);
1525 if ($result = $sth->fetchrow_hashref()) {
1526 return $result;
1529 # try default branch, default borrower category
1530 my $default_query = "SELECT maxissueqty
1531 FROM default_circ_rules";
1532 $sth = $dbh->prepare($default_query);
1533 $sth->execute();
1534 if ($result = $sth->fetchrow_hashref()) {
1535 return $result;
1538 # built-in default circulation rule
1539 return {
1540 maxissueqty => undef,
1544 =head2 GetBranchItemRule
1546 my $branch_item_rule = GetBranchItemRule($branchcode, $itemtype);
1548 Retrieves circulation rule attributes that apply to the given
1549 branch and item type, regardless of patron category.
1551 The return value is a hashref containing the following keys:
1553 holdallowed => Hold policy for this branch and itemtype. Possible values:
1554 0: No holds allowed.
1555 1: Holds allowed only by patrons that have the same homebranch as the item.
1556 2: Holds allowed from any patron.
1558 returnbranch => branch to which to return item. Possible values:
1559 noreturn: do not return, let item remain where checked in (floating collections)
1560 homebranch: return to item's home branch
1562 This searches branchitemrules in the following order:
1564 * Same branchcode and itemtype
1565 * Same branchcode, itemtype '*'
1566 * branchcode '*', same itemtype
1567 * branchcode and itemtype '*'
1569 Neither C<$branchcode> nor C<$itemtype> should be '*'.
1571 =cut
1573 sub GetBranchItemRule {
1574 my ( $branchcode, $itemtype ) = @_;
1575 my $dbh = C4::Context->dbh();
1576 my $result = {};
1578 my @attempts = (
1579 ['SELECT holdallowed, returnbranch
1580 FROM branch_item_rules
1581 WHERE branchcode = ?
1582 AND itemtype = ?', $branchcode, $itemtype],
1583 ['SELECT holdallowed, returnbranch
1584 FROM default_branch_circ_rules
1585 WHERE branchcode = ?', $branchcode],
1586 ['SELECT holdallowed, returnbranch
1587 FROM default_branch_item_rules
1588 WHERE itemtype = ?', $itemtype],
1589 ['SELECT holdallowed, returnbranch
1590 FROM default_circ_rules'],
1593 foreach my $attempt (@attempts) {
1594 my ($query, @bind_params) = @{$attempt};
1595 my $search_result = $dbh->selectrow_hashref ( $query , {}, @bind_params )
1596 or next;
1598 # Since branch/category and branch/itemtype use the same per-branch
1599 # defaults tables, we have to check that the key we want is set, not
1600 # just that a row was returned
1601 $result->{'holdallowed'} = $search_result->{'holdallowed'} unless ( defined $result->{'holdallowed'} );
1602 $result->{'returnbranch'} = $search_result->{'returnbranch'} unless ( defined $result->{'returnbranch'} );
1605 # built-in default circulation rule
1606 $result->{'holdallowed'} = 2 unless ( defined $result->{'holdallowed'} );
1607 $result->{'returnbranch'} = 'homebranch' unless ( defined $result->{'returnbranch'} );
1609 return $result;
1612 =head2 AddReturn
1614 ($doreturn, $messages, $iteminformation, $borrower) =
1615 &AddReturn($barcode, $branch, $exemptfine, $dropbox);
1617 Returns a book.
1619 =over 4
1621 =item C<$barcode> is the bar code of the book being returned.
1623 =item C<$branch> is the code of the branch where the book is being returned.
1625 =item C<$exemptfine> indicates that overdue charges for the item will be
1626 removed.
1628 =item C<$dropbox> indicates that the check-in date is assumed to be
1629 yesterday, or the last non-holiday as defined in C4::Calendar . If
1630 overdue charges are applied and C<$dropbox> is true, the last charge
1631 will be removed. This assumes that the fines accrual script has run
1632 for _today_.
1634 =back
1636 C<&AddReturn> returns a list of four items:
1638 C<$doreturn> is true iff the return succeeded.
1640 C<$messages> is a reference-to-hash giving feedback on the operation.
1641 The keys of the hash are:
1643 =over 4
1645 =item C<BadBarcode>
1647 No item with this barcode exists. The value is C<$barcode>.
1649 =item C<NotIssued>
1651 The book is not currently on loan. The value is C<$barcode>.
1653 =item C<IsPermanent>
1655 The book's home branch is a permanent collection. If you have borrowed
1656 this book, you are not allowed to return it. The value is the code for
1657 the book's home branch.
1659 =item C<wthdrawn>
1661 This book has been withdrawn/cancelled. The value should be ignored.
1663 =item C<Wrongbranch>
1665 This book has was returned to the wrong branch. The value is a hashref
1666 so that C<$messages->{Wrongbranch}->{Wrongbranch}> and C<$messages->{Wrongbranch}->{Rightbranch}>
1667 contain the branchcode of the incorrect and correct return library, respectively.
1669 =item C<ResFound>
1671 The item was reserved. The value is a reference-to-hash whose keys are
1672 fields from the reserves table of the Koha database, and
1673 C<biblioitemnumber>. It also has the key C<ResFound>, whose value is
1674 either C<Waiting>, C<Reserved>, or 0.
1676 =back
1678 C<$iteminformation> is a reference-to-hash, giving information about the
1679 returned item from the issues table.
1681 C<$borrower> is a reference-to-hash, giving information about the
1682 patron who last borrowed the book.
1684 =cut
1686 sub AddReturn {
1687 my ( $barcode, $branch, $exemptfine, $dropbox ) = @_;
1689 if ($branch and not GetBranchDetail($branch)) {
1690 warn "AddReturn error: branch '$branch' not found. Reverting to " . C4::Context->userenv->{'branch'};
1691 undef $branch;
1693 $branch = C4::Context->userenv->{'branch'} unless $branch; # we trust userenv to be a safe fallback/default
1694 my $messages;
1695 my $borrower;
1696 my $biblio;
1697 my $doreturn = 1;
1698 my $validTransfert = 0;
1699 my $stat_type = 'return';
1701 # get information on item
1702 my $itemnumber = GetItemnumberFromBarcode( $barcode );
1703 unless ($itemnumber) {
1704 return (0, { BadBarcode => $barcode }); # no barcode means no item or borrower. bail out.
1706 my $issue = GetItemIssue($itemnumber);
1707 # warn Dumper($iteminformation);
1708 if ($issue and $issue->{borrowernumber}) {
1709 $borrower = C4::Members::GetMemberDetails($issue->{borrowernumber})
1710 or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existant borrowernumber '$issue->{borrowernumber}'\n"
1711 . Dumper($issue) . "\n";
1712 } else {
1713 $messages->{'NotIssued'} = $barcode;
1714 # even though item is not on loan, it may still be transferred; therefore, get current branch info
1715 $doreturn = 0;
1716 # No issue, no borrowernumber. ONLY if $doreturn, *might* you have a $borrower later.
1717 # Record this as a local use, instead of a return, if the RecordLocalUseOnReturn is on
1718 if (C4::Context->preference("RecordLocalUseOnReturn")) {
1719 $messages->{'LocalUse'} = 1;
1720 $stat_type = 'localuse';
1724 my $item = GetItem($itemnumber) or die "GetItem($itemnumber) failed";
1725 # full item data, but no borrowernumber or checkout info (no issue)
1726 # we know GetItem should work because GetItemnumberFromBarcode worked
1727 my $hbr = GetBranchItemRule($item->{'homebranch'}, $item->{'itype'})->{'returnbranch'} || "homebranch";
1728 # get the proper branch to which to return the item
1729 $hbr = $item->{$hbr} || $branch ;
1730 # if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
1732 my $borrowernumber = $borrower->{'borrowernumber'} || undef; # we don't know if we had a borrower or not
1734 # check if the book is in a permanent collection....
1735 # FIXME -- This 'PE' attribute is largely undocumented. afaict, there's no user interface that reflects this functionality.
1736 if ( $hbr ) {
1737 my $branches = GetBranches(); # a potentially expensive call for a non-feature.
1738 $branches->{$hbr}->{PE} and $messages->{'IsPermanent'} = $hbr;
1741 # check if the return is allowed at this branch
1742 my ($returnallowed, $message) = CanBookBeReturned($item, $branch);
1743 unless ($returnallowed){
1744 $messages->{'Wrongbranch'} = {
1745 Wrongbranch => $branch,
1746 Rightbranch => $message
1748 $doreturn = 0;
1749 return ( $doreturn, $messages, $issue, $borrower );
1752 if ( $item->{'wthdrawn'} ) { # book has been cancelled
1753 $messages->{'wthdrawn'} = 1;
1754 $doreturn = 0 if C4::Context->preference("BlockReturnOfWithdrawnItems");
1757 # case of a return of document (deal with issues and holdingbranch)
1758 my $today = DateTime->now( time_zone => C4::Context->tz() );
1759 if ($doreturn) {
1760 my $datedue = $issue->{date_due};
1761 $borrower or warn "AddReturn without current borrower";
1762 my $circControlBranch;
1763 if ($dropbox) {
1764 # define circControlBranch only if dropbox mode is set
1765 # don't allow dropbox mode to create an invalid entry in issues (issuedate > today)
1766 # FIXME: check issuedate > returndate, factoring in holidays
1767 #$circControlBranch = _GetCircControlBranch($item,$borrower) unless ( $item->{'issuedate'} eq C4::Dates->today('iso') );;
1768 $circControlBranch = _GetCircControlBranch($item,$borrower);
1769 $issue->{'overdue'} = DateTime->compare($issue->{'date_due'}, $today ) == -1 ? 1 : 0;
1772 if ($borrowernumber) {
1773 if($issue->{'overdue'}){
1774 my ( $amount, $type, $unitcounttotal ) = C4::Overdues::CalcFine( $item, $borrower->{categorycode},$branch, $datedue, $today );
1775 $type ||= q{};
1776 if ( $amount > 0 && ( C4::Context->preference('finesMode') eq 'production' )) {
1777 C4::Overdues::UpdateFine(
1778 $issue->{itemnumber},
1779 $issue->{borrowernumber},
1780 $amount, $type, output_pref($datedue)
1784 MarkIssueReturned($borrowernumber, $item->{'itemnumber'}, $circControlBranch, '', $borrower->{'privacy'});
1785 $messages->{'WasReturned'} = 1; # FIXME is the "= 1" right? This could be the borrower hash.
1788 ModItem({ onloan => undef }, $issue->{'biblionumber'}, $item->{'itemnumber'});
1791 # the holdingbranch is updated if the document is returned to another location.
1792 # this is always done regardless of whether the item was on loan or not
1793 if ($item->{'holdingbranch'} ne $branch) {
1794 UpdateHoldingbranch($branch, $item->{'itemnumber'});
1795 $item->{'holdingbranch'} = $branch; # update item data holdingbranch too
1797 ModDateLastSeen( $item->{'itemnumber'} );
1799 # check if we have a transfer for this document
1800 my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->{'itemnumber'} );
1802 # if we have a transfer to do, we update the line of transfers with the datearrived
1803 if ($datesent) {
1804 if ( $tobranch eq $branch ) {
1805 my $sth = C4::Context->dbh->prepare(
1806 "UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL"
1808 $sth->execute( $item->{'itemnumber'} );
1809 # if we have a reservation with valid transfer, we can set it's status to 'W'
1810 ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
1811 C4::Reserves::ModReserveStatus($item->{'itemnumber'}, 'W');
1812 } else {
1813 $messages->{'WrongTransfer'} = $tobranch;
1814 $messages->{'WrongTransferItem'} = $item->{'itemnumber'};
1816 $validTransfert = 1;
1817 } else {
1818 ShelfToCart( $item->{'itemnumber'} ) if ( C4::Context->preference("ReturnToShelvingCart") );
1821 # fix up the accounts.....
1822 if ( $item->{'itemlost'} ) {
1823 $messages->{'WasLost'} = 1;
1825 if ( C4::Context->preference('RefundLostItemFeeOnReturn' ) ) {
1826 _FixAccountForLostAndReturned($item->{'itemnumber'}, $borrowernumber, $barcode); # can tolerate undef $borrowernumber
1827 $messages->{'LostItemFeeRefunded'} = 1;
1831 # fix up the overdues in accounts...
1832 if ($borrowernumber) {
1833 my $fix = _FixOverduesOnReturn($borrowernumber, $item->{itemnumber}, $exemptfine, $dropbox);
1834 defined($fix) or warn "_FixOverduesOnReturn($borrowernumber, $item->{itemnumber}...) failed!"; # zero is OK, check defined
1836 if ( $issue->{overdue} && $issue->{date_due} ) {
1837 # fix fine days
1838 my $debardate =
1839 _debar_user_on_return( $borrower, $item, $issue->{date_due}, $today );
1840 $messages->{Debarred} = $debardate if ($debardate);
1844 # find reserves.....
1845 # if we don't have a reserve with the status W, we launch the Checkreserves routine
1846 my ($resfound, $resrec);
1847 ($resfound, $resrec, undef) = C4::Reserves::CheckReserves( $item->{'itemnumber'} ) unless ( $item->{'wthdrawn'} );
1848 if ($resfound) {
1849 $resrec->{'ResFound'} = $resfound;
1850 $messages->{'ResFound'} = $resrec;
1853 # update stats?
1854 # Record the fact that this book was returned.
1855 UpdateStats(
1856 $branch, $stat_type, '0', '',
1857 $item->{'itemnumber'},
1858 $biblio->{'itemtype'},
1859 $borrowernumber, undef, $item->{'ccode'}
1862 # Send a check-in slip. # NOTE: borrower may be undef. probably shouldn't try to send messages then.
1863 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
1864 my %conditions = (
1865 branchcode => $branch,
1866 categorycode => $borrower->{categorycode},
1867 item_type => $item->{itype},
1868 notification => 'CHECKIN',
1870 if ($doreturn && $circulation_alert->is_enabled_for(\%conditions)) {
1871 SendCirculationAlert({
1872 type => 'CHECKIN',
1873 item => $item,
1874 borrower => $borrower,
1875 branch => $branch,
1879 logaction("CIRCULATION", "RETURN", $borrowernumber, $item->{'biblionumber'})
1880 if C4::Context->preference("ReturnLog");
1882 # FIXME: make this comment intelligible.
1883 #adding message if holdingbranch is non equal a userenv branch to return the document to homebranch
1884 #we check, if we don't have reserv or transfert for this document, if not, return it to homebranch .
1886 if (($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $hbr) and not $messages->{'WrongTransfer'}){
1887 if ( C4::Context->preference("AutomaticItemReturn" ) or
1888 (C4::Context->preference("UseBranchTransferLimits") and
1889 ! IsBranchTransferAllowed($branch, $hbr, $item->{C4::Context->preference("BranchTransferLimitsType")} )
1890 )) {
1891 $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s)", $item->{'itemnumber'},$branch, $hbr;
1892 $debug and warn "item: " . Dumper($item);
1893 ModItemTransfer($item->{'itemnumber'}, $branch, $hbr);
1894 $messages->{'WasTransfered'} = 1;
1895 } else {
1896 $messages->{'NeedsTransfer'} = 1; # TODO: instead of 1, specify branchcode that the transfer SHOULD go to, $item->{homebranch}
1899 return ( $doreturn, $messages, $issue, $borrower );
1902 =head2 MarkIssueReturned
1904 MarkIssueReturned($borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy);
1906 Unconditionally marks an issue as being returned by
1907 moving the C<issues> row to C<old_issues> and
1908 setting C<returndate> to the current date, or
1909 the last non-holiday date of the branccode specified in
1910 C<dropbox_branch> . Assumes you've already checked that
1911 it's safe to do this, i.e. last non-holiday > issuedate.
1913 if C<$returndate> is specified (in iso format), it is used as the date
1914 of the return. It is ignored when a dropbox_branch is passed in.
1916 C<$privacy> contains the privacy parameter. If the patron has set privacy to 2,
1917 the old_issue is immediately anonymised
1919 Ideally, this function would be internal to C<C4::Circulation>,
1920 not exported, but it is currently needed by one
1921 routine in C<C4::Accounts>.
1923 =cut
1925 sub MarkIssueReturned {
1926 my ( $borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy ) = @_;
1928 my $dbh = C4::Context->dbh;
1929 my $query = 'UPDATE issues SET returndate=';
1930 my @bind;
1931 if ($dropbox_branch) {
1932 my $calendar = Koha::Calendar->new( branchcode => $dropbox_branch );
1933 my $dropboxdate = $calendar->addDate( DateTime->now( time_zone => C4::Context->tz), -1 );
1934 $query .= ' ? ';
1935 push @bind, $dropboxdate->strftime('%Y-%m-%d %H:%M');
1936 } elsif ($returndate) {
1937 $query .= ' ? ';
1938 push @bind, $returndate;
1939 } else {
1940 $query .= ' now() ';
1942 $query .= ' WHERE borrowernumber = ? AND itemnumber = ?';
1943 push @bind, $borrowernumber, $itemnumber;
1944 # FIXME transaction
1945 my $sth_upd = $dbh->prepare($query);
1946 $sth_upd->execute(@bind);
1947 my $sth_copy = $dbh->prepare('INSERT INTO old_issues SELECT * FROM issues
1948 WHERE borrowernumber = ?
1949 AND itemnumber = ?');
1950 $sth_copy->execute($borrowernumber, $itemnumber);
1951 # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber
1952 if ( $privacy == 2) {
1953 # The default of 0 does not work due to foreign key constraints
1954 # The anonymisation will fail quietly if AnonymousPatron is not a valid entry
1955 my $anonymouspatron = (C4::Context->preference('AnonymousPatron')) ? C4::Context->preference('AnonymousPatron') : 0;
1956 my $sth_ano = $dbh->prepare("UPDATE old_issues SET borrowernumber=?
1957 WHERE borrowernumber = ?
1958 AND itemnumber = ?");
1959 $sth_ano->execute($anonymouspatron, $borrowernumber, $itemnumber);
1961 my $sth_del = $dbh->prepare("DELETE FROM issues
1962 WHERE borrowernumber = ?
1963 AND itemnumber = ?");
1964 $sth_del->execute($borrowernumber, $itemnumber);
1967 =head2 _debar_user_on_return
1969 _debar_user_on_return($borrower, $item, $datedue, today);
1971 C<$borrower> borrower hashref
1973 C<$item> item hashref
1975 C<$datedue> date due DateTime object
1977 C<$today> DateTime object representing the return time
1979 Internal function, called only by AddReturn that calculates and updates
1980 the user fine days, and debars him if necessary.
1982 Should only be called for overdue returns
1984 =cut
1986 sub _debar_user_on_return {
1987 my ( $borrower, $item, $dt_due, $dt_today ) = @_;
1989 my $branchcode = _GetCircControlBranch( $item, $borrower );
1990 my $calendar = Koha::Calendar->new( branchcode => $branchcode );
1992 # $deltadays is a DateTime::Duration object
1993 my $deltadays = $calendar->days_between( $dt_due, $dt_today );
1995 my $circcontrol = C4::Context->preference('CircControl');
1996 my $issuingrule =
1997 GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode );
1998 my $finedays = $issuingrule->{finedays};
1999 my $unit = $issuingrule->{lengthunit};
2001 if ($finedays) {
2003 # finedays is in days, so hourly loans must multiply by 24
2004 # thus 1 hour late equals 1 day suspension * finedays rate
2005 $finedays = $finedays * 24 if ( $unit eq 'hours' );
2007 # grace period is measured in the same units as the loan
2008 my $grace =
2009 DateTime::Duration->new( $unit => $issuingrule->{firstremind} );
2010 if ( $deltadays->subtract($grace)->is_positive() ) {
2012 my $new_debar_dt =
2013 $dt_today->clone()->add_duration( $deltadays * $finedays );
2014 if ( $borrower->{debarred} ) {
2015 my $borrower_debar_dt = dt_from_string( $borrower->{debarred} );
2017 # Update patron only if new date > old
2018 if ( DateTime->compare( $borrower_debar_dt, $new_debar_dt ) !=
2019 -1 )
2021 return;
2025 C4::Members::DebarMember( $borrower->{borrowernumber},
2026 $new_debar_dt->ymd() );
2027 return $new_debar_dt->ymd();
2030 return;
2033 =head2 _FixOverduesOnReturn
2035 &_FixOverduesOnReturn($brn,$itm, $exemptfine, $dropboxmode);
2037 C<$brn> borrowernumber
2039 C<$itm> itemnumber
2041 C<$exemptfine> BOOL -- remove overdue charge associated with this issue.
2042 C<$dropboxmode> BOOL -- remove lastincrement on overdue charge associated with this issue.
2044 Internal function, called only by AddReturn
2046 =cut
2048 sub _FixOverduesOnReturn {
2049 my ($borrowernumber, $item);
2050 unless ($borrowernumber = shift) {
2051 warn "_FixOverduesOnReturn() not supplied valid borrowernumber";
2052 return;
2054 unless ($item = shift) {
2055 warn "_FixOverduesOnReturn() not supplied valid itemnumber";
2056 return;
2058 my ($exemptfine, $dropbox) = @_;
2059 my $dbh = C4::Context->dbh;
2061 # check for overdue fine
2062 my $sth = $dbh->prepare(
2063 "SELECT * FROM accountlines WHERE (borrowernumber = ?) AND (itemnumber = ?) AND (accounttype='FU' OR accounttype='O')"
2065 $sth->execute( $borrowernumber, $item );
2067 # alter fine to show that the book has been returned
2068 my $data = $sth->fetchrow_hashref;
2069 return 0 unless $data; # no warning, there's just nothing to fix
2071 my $uquery;
2072 my @bind = ($data->{'accountlines_id'});
2073 if ($exemptfine) {
2074 $uquery = "update accountlines set accounttype='FFOR', amountoutstanding=0";
2075 if (C4::Context->preference("FinesLog")) {
2076 &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item");
2078 } elsif ($dropbox && $data->{lastincrement}) {
2079 my $outstanding = $data->{amountoutstanding} - $data->{lastincrement} ;
2080 my $amt = $data->{amount} - $data->{lastincrement} ;
2081 if (C4::Context->preference("FinesLog")) {
2082 &logaction("FINES", 'MODIFY',$borrowernumber,"Dropbox adjustment $amt, item $item");
2084 $uquery = "update accountlines set accounttype='F' ";
2085 if($outstanding >= 0 && $amt >=0) {
2086 $uquery .= ", amount = ? , amountoutstanding=? ";
2087 unshift @bind, ($amt, $outstanding) ;
2089 } else {
2090 $uquery = "update accountlines set accounttype='F' ";
2092 $uquery .= " where (accountlines_id = ?)";
2093 my $usth = $dbh->prepare($uquery);
2094 return $usth->execute(@bind);
2097 =head2 _FixAccountForLostAndReturned
2099 &_FixAccountForLostAndReturned($itemnumber, [$borrowernumber, $barcode]);
2101 Calculates the charge for a book lost and returned.
2103 Internal function, not exported, called only by AddReturn.
2105 FIXME: This function reflects how inscrutable fines logic is. Fix both.
2106 FIXME: Give a positive return value on success. It might be the $borrowernumber who received credit, or the amount forgiven.
2108 =cut
2110 sub _FixAccountForLostAndReturned {
2111 my $itemnumber = shift or return;
2112 my $borrowernumber = @_ ? shift : undef;
2113 my $item_id = @_ ? shift : $itemnumber; # Send the barcode if you want that logged in the description
2114 my $dbh = C4::Context->dbh;
2115 # check for charge made for lost book
2116 my $sth = $dbh->prepare("SELECT * FROM accountlines WHERE itemnumber = ? AND accounttype IN ('L', 'Rep', 'W') ORDER BY date DESC, accountno DESC");
2117 $sth->execute($itemnumber);
2118 my $data = $sth->fetchrow_hashref;
2119 $data or return; # bail if there is nothing to do
2120 $data->{accounttype} eq 'W' and return; # Written off
2122 # writeoff this amount
2123 my $offset;
2124 my $amount = $data->{'amount'};
2125 my $acctno = $data->{'accountno'};
2126 my $amountleft; # Starts off undef/zero.
2127 if ($data->{'amountoutstanding'} == $amount) {
2128 $offset = $data->{'amount'};
2129 $amountleft = 0; # Hey, it's zero here, too.
2130 } else {
2131 $offset = $amount - $data->{'amountoutstanding'}; # Um, isn't this the same as ZERO? We just tested those two things are ==
2132 $amountleft = $data->{'amountoutstanding'} - $amount; # Um, isn't this the same as ZERO? We just tested those two things are ==
2134 my $usth = $dbh->prepare("UPDATE accountlines SET accounttype = 'LR',amountoutstanding='0'
2135 WHERE (accountlines_id = ?)");
2136 $usth->execute($data->{'accountlines_id'}); # We might be adjusting an account for some OTHER borrowernumber now. Not the one we passed in.
2137 #check if any credit is left if so writeoff other accounts
2138 my $nextaccntno = getnextacctno($data->{'borrowernumber'});
2139 $amountleft *= -1 if ($amountleft < 0);
2140 if ($amountleft > 0) {
2141 my $msth = $dbh->prepare("SELECT * FROM accountlines WHERE (borrowernumber = ?)
2142 AND (amountoutstanding >0) ORDER BY date"); # might want to order by amountoustanding ASC (pay smallest first)
2143 $msth->execute($data->{'borrowernumber'});
2144 # offset transactions
2145 my $newamtos;
2146 my $accdata;
2147 while (($accdata=$msth->fetchrow_hashref) and ($amountleft>0)){
2148 if ($accdata->{'amountoutstanding'} < $amountleft) {
2149 $newamtos = 0;
2150 $amountleft -= $accdata->{'amountoutstanding'};
2151 } else {
2152 $newamtos = $accdata->{'amountoutstanding'} - $amountleft;
2153 $amountleft = 0;
2155 my $thisacct = $accdata->{'accountlines_id'};
2156 # FIXME: move prepares outside while loop!
2157 my $usth = $dbh->prepare("UPDATE accountlines SET amountoutstanding= ?
2158 WHERE (accountlines_id = ?)");
2159 $usth->execute($newamtos,'$thisacct'); # FIXME: '$thisacct' is a string literal!
2160 $usth = $dbh->prepare("INSERT INTO accountoffsets
2161 (borrowernumber, accountno, offsetaccount, offsetamount)
2162 VALUES
2163 (?,?,?,?)");
2164 $usth->execute($data->{'borrowernumber'},$accdata->{'accountno'},$nextaccntno,$newamtos);
2166 $msth->finish; # $msth might actually have data left
2168 $amountleft *= -1 if ($amountleft > 0);
2169 my $desc = "Item Returned " . $item_id;
2170 $usth = $dbh->prepare("INSERT INTO accountlines
2171 (borrowernumber,accountno,date,amount,description,accounttype,amountoutstanding)
2172 VALUES (?,?,now(),?,?,'CR',?)");
2173 $usth->execute($data->{'borrowernumber'},$nextaccntno,0-$amount,$desc,$amountleft);
2174 if ($borrowernumber) {
2175 # FIXME: same as query above. use 1 sth for both
2176 $usth = $dbh->prepare("INSERT INTO accountoffsets
2177 (borrowernumber, accountno, offsetaccount, offsetamount)
2178 VALUES (?,?,?,?)");
2179 $usth->execute($borrowernumber, $data->{'accountno'}, $nextaccntno, $offset);
2181 ModItem({ paidfor => '' }, undef, $itemnumber);
2182 return;
2185 =head2 _GetCircControlBranch
2187 my $circ_control_branch = _GetCircControlBranch($iteminfos, $borrower);
2189 Internal function :
2191 Return the library code to be used to determine which circulation
2192 policy applies to a transaction. Looks up the CircControl and
2193 HomeOrHoldingBranch system preferences.
2195 C<$iteminfos> is a hashref to iteminfo. Only {homebranch or holdingbranch} is used.
2197 C<$borrower> is a hashref to borrower. Only {branchcode} is used.
2199 =cut
2201 sub _GetCircControlBranch {
2202 my ($item, $borrower) = @_;
2203 my $circcontrol = C4::Context->preference('CircControl');
2204 my $branch;
2206 if ($circcontrol eq 'PickupLibrary' and (C4::Context->userenv and C4::Context->userenv->{'branch'}) ) {
2207 $branch= C4::Context->userenv->{'branch'};
2208 } elsif ($circcontrol eq 'PatronLibrary') {
2209 $branch=$borrower->{branchcode};
2210 } else {
2211 my $branchfield = C4::Context->preference('HomeOrHoldingBranch') || 'homebranch';
2212 $branch = $item->{$branchfield};
2213 # default to item home branch if holdingbranch is used
2214 # and is not defined
2215 if (!defined($branch) && $branchfield eq 'holdingbranch') {
2216 $branch = $item->{homebranch};
2219 return $branch;
2227 =head2 GetItemIssue
2229 $issue = &GetItemIssue($itemnumber);
2231 Returns patron currently having a book, or undef if not checked out.
2233 C<$itemnumber> is the itemnumber.
2235 C<$issue> is a hashref of the row from the issues table.
2237 =cut
2239 sub GetItemIssue {
2240 my ($itemnumber) = @_;
2241 return unless $itemnumber;
2242 my $sth = C4::Context->dbh->prepare(
2243 "SELECT *
2244 FROM issues
2245 LEFT JOIN items ON issues.itemnumber=items.itemnumber
2246 WHERE issues.itemnumber=?");
2247 $sth->execute($itemnumber);
2248 my $data = $sth->fetchrow_hashref;
2249 return unless $data;
2250 $data->{issuedate} = dt_from_string($data->{issuedate}, 'sql');
2251 $data->{issuedate}->truncate(to => 'minute');
2252 $data->{date_due} = dt_from_string($data->{date_due}, 'sql');
2253 $data->{date_due}->truncate(to => 'minute');
2254 my $dt = DateTime->now( time_zone => C4::Context->tz)->truncate( to => 'minute');
2255 $data->{'overdue'} = DateTime->compare($data->{'date_due'}, $dt ) == -1 ? 1 : 0;
2256 return $data;
2259 =head2 GetOpenIssue
2261 $issue = GetOpenIssue( $itemnumber );
2263 Returns the row from the issues table if the item is currently issued, undef if the item is not currently issued
2265 C<$itemnumber> is the item's itemnumber
2267 Returns a hashref
2269 =cut
2271 sub GetOpenIssue {
2272 my ( $itemnumber ) = @_;
2274 my $dbh = C4::Context->dbh;
2275 my $sth = $dbh->prepare( "SELECT * FROM issues WHERE itemnumber = ? AND returndate IS NULL" );
2276 $sth->execute( $itemnumber );
2277 my $issue = $sth->fetchrow_hashref();
2278 return $issue;
2281 =head2 GetItemIssues
2283 $issues = &GetItemIssues($itemnumber, $history);
2285 Returns patrons that have issued a book
2287 C<$itemnumber> is the itemnumber
2288 C<$history> is false if you just want the current "issuer" (if any)
2289 and true if you want issues history from old_issues also.
2291 Returns reference to an array of hashes
2293 =cut
2295 sub GetItemIssues {
2296 my ( $itemnumber, $history ) = @_;
2298 my $today = DateTime->now( time_zome => C4::Context->tz); # get today date
2299 $today->truncate( to => 'minute' );
2300 my $sql = "SELECT * FROM issues
2301 JOIN borrowers USING (borrowernumber)
2302 JOIN items USING (itemnumber)
2303 WHERE issues.itemnumber = ? ";
2304 if ($history) {
2305 $sql .= "UNION ALL
2306 SELECT * FROM old_issues
2307 LEFT JOIN borrowers USING (borrowernumber)
2308 JOIN items USING (itemnumber)
2309 WHERE old_issues.itemnumber = ? ";
2311 $sql .= "ORDER BY date_due DESC";
2312 my $sth = C4::Context->dbh->prepare($sql);
2313 if ($history) {
2314 $sth->execute($itemnumber, $itemnumber);
2315 } else {
2316 $sth->execute($itemnumber);
2318 my $results = $sth->fetchall_arrayref({});
2319 foreach (@$results) {
2320 my $date_due = dt_from_string($_->{date_due},'sql');
2321 $date_due->truncate( to => 'minute' );
2323 $_->{overdue} = (DateTime->compare($date_due, $today) == -1) ? 1 : 0;
2325 return $results;
2328 =head2 GetBiblioIssues
2330 $issues = GetBiblioIssues($biblionumber);
2332 this function get all issues from a biblionumber.
2334 Return:
2335 C<$issues> is a reference to array which each value is ref-to-hash. This ref-to-hash containts all column from
2336 tables issues and the firstname,surname & cardnumber from borrowers.
2338 =cut
2340 sub GetBiblioIssues {
2341 my $biblionumber = shift;
2342 return unless $biblionumber;
2343 my $dbh = C4::Context->dbh;
2344 my $query = "
2345 SELECT issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2346 FROM issues
2347 LEFT JOIN borrowers ON borrowers.borrowernumber = issues.borrowernumber
2348 LEFT JOIN items ON issues.itemnumber = items.itemnumber
2349 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2350 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2351 WHERE biblio.biblionumber = ?
2352 UNION ALL
2353 SELECT old_issues.*,items.barcode,biblio.biblionumber,biblio.title, biblio.author,borrowers.cardnumber,borrowers.surname,borrowers.firstname
2354 FROM old_issues
2355 LEFT JOIN borrowers ON borrowers.borrowernumber = old_issues.borrowernumber
2356 LEFT JOIN items ON old_issues.itemnumber = items.itemnumber
2357 LEFT JOIN biblioitems ON items.itemnumber = biblioitems.biblioitemnumber
2358 LEFT JOIN biblio ON biblio.biblionumber = items.biblionumber
2359 WHERE biblio.biblionumber = ?
2360 ORDER BY timestamp
2362 my $sth = $dbh->prepare($query);
2363 $sth->execute($biblionumber, $biblionumber);
2365 my @issues;
2366 while ( my $data = $sth->fetchrow_hashref ) {
2367 push @issues, $data;
2369 return \@issues;
2372 =head2 GetUpcomingDueIssues
2374 my $upcoming_dues = GetUpcomingDueIssues( { days_in_advance => 4 } );
2376 =cut
2378 sub GetUpcomingDueIssues {
2379 my $params = shift;
2381 $params->{'days_in_advance'} = 7 unless exists $params->{'days_in_advance'};
2382 my $dbh = C4::Context->dbh;
2384 my $statement = <<END_SQL;
2385 SELECT issues.*, items.itype as itemtype, items.homebranch, TO_DAYS( date_due )-TO_DAYS( NOW() ) as days_until_due, branches.branchemail
2386 FROM issues
2387 LEFT JOIN items USING (itemnumber)
2388 LEFT OUTER JOIN branches USING (branchcode)
2389 WhERE returndate is NULL
2390 AND ( TO_DAYS( NOW() )-TO_DAYS( date_due ) ) < ?
2391 END_SQL
2393 my @bind_parameters = ( $params->{'days_in_advance'} );
2395 my $sth = $dbh->prepare( $statement );
2396 $sth->execute( @bind_parameters );
2397 my $upcoming_dues = $sth->fetchall_arrayref({});
2398 $sth->finish;
2400 return $upcoming_dues;
2403 =head2 CanBookBeRenewed
2405 ($ok,$error) = &CanBookBeRenewed($borrowernumber, $itemnumber[, $override_limit]);
2407 Find out whether a borrowed item may be renewed.
2409 C<$dbh> is a DBI handle to the Koha database.
2411 C<$borrowernumber> is the borrower number of the patron who currently
2412 has the item on loan.
2414 C<$itemnumber> is the number of the item to renew.
2416 C<$override_limit>, if supplied with a true value, causes
2417 the limit on the number of times that the loan can be renewed
2418 (as controlled by the item type) to be ignored.
2420 C<$CanBookBeRenewed> returns a true value iff the item may be renewed. The
2421 item must currently be on loan to the specified borrower; renewals
2422 must be allowed for the item's type; and the borrower must not have
2423 already renewed the loan. $error will contain the reason the renewal can not proceed
2425 =cut
2427 sub CanBookBeRenewed {
2429 # check renewal status
2430 my ( $borrowernumber, $itemnumber, $override_limit ) = @_;
2431 my $dbh = C4::Context->dbh;
2432 my $renews = 1;
2433 my $renewokay = 0;
2434 my $error;
2436 # Look in the issues table for this item, lent to this borrower,
2437 # and not yet returned.
2439 # Look in the issues table for this item, lent to this borrower,
2440 # and not yet returned.
2441 my %branch = (
2442 'ItemHomeLibrary' => 'items.homebranch',
2443 'PickupLibrary' => 'items.holdingbranch',
2444 'PatronLibrary' => 'borrowers.branchcode'
2446 my $controlbranch = $branch{C4::Context->preference('CircControl')};
2447 my $itype = C4::Context->preference('item-level_itypes') ? 'items.itype' : 'biblioitems.itemtype';
2449 my $sthcount = $dbh->prepare("
2450 SELECT
2451 borrowers.categorycode, biblioitems.itemtype, issues.renewals, renewalsallowed, $controlbranch
2452 FROM issuingrules,
2453 issues
2454 LEFT JOIN items USING (itemnumber)
2455 LEFT JOIN borrowers USING (borrowernumber)
2456 LEFT JOIN biblioitems USING (biblioitemnumber)
2458 WHERE
2459 (issuingrules.categorycode = borrowers.categorycode OR issuingrules.categorycode = '*')
2461 (issuingrules.itemtype = $itype OR issuingrules.itemtype = '*')
2463 (issuingrules.branchcode = $controlbranch OR issuingrules.branchcode = '*')
2464 AND
2465 borrowernumber = ?
2467 itemnumber = ?
2468 ORDER BY
2469 issuingrules.categorycode desc,
2470 issuingrules.itemtype desc,
2471 issuingrules.branchcode desc
2472 LIMIT 1;
2475 $sthcount->execute( $borrowernumber, $itemnumber );
2476 if ( my $data1 = $sthcount->fetchrow_hashref ) {
2478 if ( ( $data1->{renewalsallowed} && $data1->{renewalsallowed} > $data1->{renewals} ) || $override_limit ) {
2479 $renewokay = 1;
2481 else {
2482 $error="too_many";
2485 my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber);
2486 if ($resfound) {
2487 $renewokay = 0;
2488 $error="on_reserve"
2492 return ($renewokay,$error);
2495 =head2 AddRenewal
2497 &AddRenewal($borrowernumber, $itemnumber, $branch, [$datedue], [$lastreneweddate]);
2499 Renews a loan.
2501 C<$borrowernumber> is the borrower number of the patron who currently
2502 has the item.
2504 C<$itemnumber> is the number of the item to renew.
2506 C<$branch> is the library where the renewal took place (if any).
2507 The library that controls the circ policies for the renewal is retrieved from the issues record.
2509 C<$datedue> can be a C4::Dates object used to set the due date.
2511 C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate. If
2512 this parameter is not supplied, lastreneweddate is set to the current date.
2514 If C<$datedue> is the empty string, C<&AddRenewal> will calculate the due date automatically
2515 from the book's item type.
2517 =cut
2519 sub AddRenewal {
2520 my $borrowernumber = shift or return;
2521 my $itemnumber = shift or return;
2522 my $branch = shift;
2523 my $datedue = shift;
2524 my $lastreneweddate = shift || DateTime->now(time_zone => C4::Context->tz)->ymd();
2525 my $item = GetItem($itemnumber) or return;
2526 my $biblio = GetBiblioFromItemNumber($itemnumber) or return;
2528 my $dbh = C4::Context->dbh;
2529 # Find the issues record for this book
2530 my $sth =
2531 $dbh->prepare("SELECT * FROM issues
2532 WHERE borrowernumber=?
2533 AND itemnumber=?"
2535 $sth->execute( $borrowernumber, $itemnumber );
2536 my $issuedata = $sth->fetchrow_hashref;
2537 $sth->finish;
2538 if(defined $datedue && ref $datedue ne 'DateTime' ) {
2539 carp 'Invalid date passed to AddRenewal.';
2540 return;
2542 # If the due date wasn't specified, calculate it by adding the
2543 # book's loan length to today's date or the current due date
2544 # based on the value of the RenewalPeriodBase syspref.
2545 unless ($datedue) {
2547 my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber ) or return;
2548 my $itemtype = (C4::Context->preference('item-level_itypes')) ? $biblio->{'itype'} : $biblio->{'itemtype'};
2550 $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ?
2551 $issuedata->{date_due} :
2552 DateTime->now( time_zone => C4::Context->tz());
2553 $datedue = CalcDateDue($datedue,$itemtype,$issuedata->{'branchcode'},$borrower);
2556 # Update the issues record to have the new due date, and a new count
2557 # of how many times it has been renewed.
2558 my $renews = $issuedata->{'renewals'} + 1;
2559 $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ?
2560 WHERE borrowernumber=?
2561 AND itemnumber=?"
2564 $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $lastreneweddate, $borrowernumber, $itemnumber );
2565 $sth->finish;
2567 # Update the renewal count on the item, and tell zebra to reindex
2568 $renews = $biblio->{'renewals'} + 1;
2569 ModItem({ renewals => $renews, onloan => $datedue->strftime('%Y-%m-%d %H:%M')}, $biblio->{'biblionumber'}, $itemnumber);
2571 # Charge a new rental fee, if applicable?
2572 my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
2573 if ( $charge > 0 ) {
2574 my $accountno = getnextacctno( $borrowernumber );
2575 my $item = GetBiblioFromItemNumber($itemnumber);
2576 my $manager_id = 0;
2577 $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
2578 $sth = $dbh->prepare(
2579 "INSERT INTO accountlines
2580 (date, borrowernumber, accountno, amount, manager_id,
2581 description,accounttype, amountoutstanding, itemnumber)
2582 VALUES (now(),?,?,?,?,?,?,?,?)"
2584 $sth->execute( $borrowernumber, $accountno, $charge, $manager_id,
2585 "Renewal of Rental Item $item->{'title'} $item->{'barcode'}",
2586 'Rent', $charge, $itemnumber );
2589 # Send a renewal slip according to checkout alert preferencei
2590 if ( C4::Context->preference('RenewalSendNotice') eq '1') {
2591 my $borrower = C4::Members::GetMemberDetails( $borrowernumber, 0 );
2592 my $circulation_alert = 'C4::ItemCirculationAlertPreference';
2593 my %conditions = (
2594 branchcode => $branch,
2595 categorycode => $borrower->{categorycode},
2596 item_type => $item->{itype},
2597 notification => 'CHECKOUT',
2599 if ($circulation_alert->is_enabled_for(\%conditions)) {
2600 SendCirculationAlert({
2601 type => 'RENEWAL',
2602 item => $item,
2603 borrower => $borrower,
2604 branch => $branch,
2609 # Log the renewal
2610 UpdateStats( $branch, 'renew', $charge, '', $itemnumber, $item->{itype}, $borrowernumber, undef, $item->{'ccode'});
2611 return $datedue;
2614 sub GetRenewCount {
2615 # check renewal status
2616 my ( $bornum, $itemno ) = @_;
2617 my $dbh = C4::Context->dbh;
2618 my $renewcount = 0;
2619 my $renewsallowed = 0;
2620 my $renewsleft = 0;
2622 my $borrower = C4::Members::GetMember( borrowernumber => $bornum);
2623 my $item = GetItem($itemno);
2625 # Look in the issues table for this item, lent to this borrower,
2626 # and not yet returned.
2628 # FIXME - I think this function could be redone to use only one SQL call.
2629 my $sth = $dbh->prepare(
2630 "select * from issues
2631 where (borrowernumber = ?)
2632 and (itemnumber = ?)"
2634 $sth->execute( $bornum, $itemno );
2635 my $data = $sth->fetchrow_hashref;
2636 $renewcount = $data->{'renewals'} if $data->{'renewals'};
2637 $sth->finish;
2638 # $item and $borrower should be calculated
2639 my $branchcode = _GetCircControlBranch($item, $borrower);
2641 my $issuingrule = GetIssuingRule($borrower->{categorycode}, $item->{itype}, $branchcode);
2643 $renewsallowed = $issuingrule->{'renewalsallowed'};
2644 $renewsleft = $renewsallowed - $renewcount;
2645 if($renewsleft < 0){ $renewsleft = 0; }
2646 return ( $renewcount, $renewsallowed, $renewsleft );
2649 =head2 GetIssuingCharges
2651 ($charge, $item_type) = &GetIssuingCharges($itemnumber, $borrowernumber);
2653 Calculate how much it would cost for a given patron to borrow a given
2654 item, including any applicable discounts.
2656 C<$itemnumber> is the item number of item the patron wishes to borrow.
2658 C<$borrowernumber> is the patron's borrower number.
2660 C<&GetIssuingCharges> returns two values: C<$charge> is the rental charge,
2661 and C<$item_type> is the code for the item's item type (e.g., C<VID>
2662 if it's a video).
2664 =cut
2666 sub GetIssuingCharges {
2668 # calculate charges due
2669 my ( $itemnumber, $borrowernumber ) = @_;
2670 my $charge = 0;
2671 my $dbh = C4::Context->dbh;
2672 my $item_type;
2674 # Get the book's item type and rental charge (via its biblioitem).
2675 my $charge_query = 'SELECT itemtypes.itemtype,rentalcharge FROM items
2676 LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber';
2677 $charge_query .= (C4::Context->preference('item-level_itypes'))
2678 ? ' LEFT JOIN itemtypes ON items.itype = itemtypes.itemtype'
2679 : ' LEFT JOIN itemtypes ON biblioitems.itemtype = itemtypes.itemtype';
2681 $charge_query .= ' WHERE items.itemnumber =?';
2683 my $sth = $dbh->prepare($charge_query);
2684 $sth->execute($itemnumber);
2685 if ( my $item_data = $sth->fetchrow_hashref ) {
2686 $item_type = $item_data->{itemtype};
2687 $charge = $item_data->{rentalcharge};
2688 my $branch = C4::Branch::mybranch();
2689 my $discount_query = q|SELECT rentaldiscount,
2690 issuingrules.itemtype, issuingrules.branchcode
2691 FROM borrowers
2692 LEFT JOIN issuingrules ON borrowers.categorycode = issuingrules.categorycode
2693 WHERE borrowers.borrowernumber = ?
2694 AND (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
2695 AND (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')|;
2696 my $discount_sth = $dbh->prepare($discount_query);
2697 $discount_sth->execute( $borrowernumber, $item_type, $branch );
2698 my $discount_rules = $discount_sth->fetchall_arrayref({});
2699 if (@{$discount_rules}) {
2700 # We may have multiple rules so get the most specific
2701 my $discount = _get_discount_from_rule($discount_rules, $branch, $item_type);
2702 $charge = ( $charge * ( 100 - $discount ) ) / 100;
2706 $sth->finish; # we havent _explicitly_ fetched all rows
2707 return ( $charge, $item_type );
2710 # Select most appropriate discount rule from those returned
2711 sub _get_discount_from_rule {
2712 my ($rules_ref, $branch, $itemtype) = @_;
2713 my $discount;
2715 if (@{$rules_ref} == 1) { # only 1 applicable rule use it
2716 $discount = $rules_ref->[0]->{rentaldiscount};
2717 return (defined $discount) ? $discount : 0;
2719 # could have up to 4 does one match $branch and $itemtype
2720 my @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq $itemtype } @{$rules_ref};
2721 if (@d) {
2722 $discount = $d[0]->{rentaldiscount};
2723 return (defined $discount) ? $discount : 0;
2725 # do we have item type + all branches
2726 @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq $itemtype } @{$rules_ref};
2727 if (@d) {
2728 $discount = $d[0]->{rentaldiscount};
2729 return (defined $discount) ? $discount : 0;
2731 # do we all item types + this branch
2732 @d = grep { $_->{branchcode} eq $branch && $_->{itemtype} eq q{*} } @{$rules_ref};
2733 if (@d) {
2734 $discount = $d[0]->{rentaldiscount};
2735 return (defined $discount) ? $discount : 0;
2737 # so all and all (surely we wont get here)
2738 @d = grep { $_->{branchcode} eq q{*} && $_->{itemtype} eq q{*} } @{$rules_ref};
2739 if (@d) {
2740 $discount = $d[0]->{rentaldiscount};
2741 return (defined $discount) ? $discount : 0;
2743 # none of the above
2744 return 0;
2747 =head2 AddIssuingCharge
2749 &AddIssuingCharge( $itemno, $borrowernumber, $charge )
2751 =cut
2753 sub AddIssuingCharge {
2754 my ( $itemnumber, $borrowernumber, $charge ) = @_;
2755 my $dbh = C4::Context->dbh;
2756 my $nextaccntno = getnextacctno( $borrowernumber );
2757 my $manager_id = 0;
2758 $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv;
2759 my $query ="
2760 INSERT INTO accountlines
2761 (borrowernumber, itemnumber, accountno,
2762 date, amount, description, accounttype,
2763 amountoutstanding, manager_id)
2764 VALUES (?, ?, ?,now(), ?, 'Rental', 'Rent',?,?)
2766 my $sth = $dbh->prepare($query);
2767 $sth->execute( $borrowernumber, $itemnumber, $nextaccntno, $charge, $charge, $manager_id );
2768 $sth->finish;
2771 =head2 GetTransfers
2773 GetTransfers($itemnumber);
2775 =cut
2777 sub GetTransfers {
2778 my ($itemnumber) = @_;
2780 my $dbh = C4::Context->dbh;
2782 my $query = '
2783 SELECT datesent,
2784 frombranch,
2785 tobranch
2786 FROM branchtransfers
2787 WHERE itemnumber = ?
2788 AND datearrived IS NULL
2790 my $sth = $dbh->prepare($query);
2791 $sth->execute($itemnumber);
2792 my @row = $sth->fetchrow_array();
2793 $sth->finish;
2794 return @row;
2797 =head2 GetTransfersFromTo
2799 @results = GetTransfersFromTo($frombranch,$tobranch);
2801 Returns the list of pending transfers between $from and $to branch
2803 =cut
2805 sub GetTransfersFromTo {
2806 my ( $frombranch, $tobranch ) = @_;
2807 return unless ( $frombranch && $tobranch );
2808 my $dbh = C4::Context->dbh;
2809 my $query = "
2810 SELECT itemnumber,datesent,frombranch
2811 FROM branchtransfers
2812 WHERE frombranch=?
2813 AND tobranch=?
2814 AND datearrived IS NULL
2816 my $sth = $dbh->prepare($query);
2817 $sth->execute( $frombranch, $tobranch );
2818 my @gettransfers;
2820 while ( my $data = $sth->fetchrow_hashref ) {
2821 push @gettransfers, $data;
2823 $sth->finish;
2824 return (@gettransfers);
2827 =head2 DeleteTransfer
2829 &DeleteTransfer($itemnumber);
2831 =cut
2833 sub DeleteTransfer {
2834 my ($itemnumber) = @_;
2835 my $dbh = C4::Context->dbh;
2836 my $sth = $dbh->prepare(
2837 "DELETE FROM branchtransfers
2838 WHERE itemnumber=?
2839 AND datearrived IS NULL "
2841 $sth->execute($itemnumber);
2842 $sth->finish;
2845 =head2 AnonymiseIssueHistory
2847 $rows = AnonymiseIssueHistory($date,$borrowernumber)
2849 This function write NULL instead of C<$borrowernumber> given on input arg into the table issues.
2850 if C<$borrowernumber> is not set, it will delete the issue history for all borrower older than C<$date>.
2852 If c<$borrowernumber> is set, it will delete issue history for only that borrower, regardless of their opac privacy
2853 setting (force delete).
2855 return the number of affected rows.
2857 =cut
2859 sub AnonymiseIssueHistory {
2860 my $date = shift;
2861 my $borrowernumber = shift;
2862 my $dbh = C4::Context->dbh;
2863 my $query = "
2864 UPDATE old_issues
2865 SET borrowernumber = ?
2866 WHERE returndate < ?
2867 AND borrowernumber IS NOT NULL
2870 # The default of 0 does not work due to foreign key constraints
2871 # The anonymisation will fail quietly if AnonymousPatron is not a valid entry
2872 my $anonymouspatron = (C4::Context->preference('AnonymousPatron')) ? C4::Context->preference('AnonymousPatron') : 0;
2873 my @bind_params = ($anonymouspatron, $date);
2874 if (defined $borrowernumber) {
2875 $query .= " AND borrowernumber = ?";
2876 push @bind_params, $borrowernumber;
2877 } else {
2878 $query .= " AND (SELECT privacy FROM borrowers WHERE borrowers.borrowernumber=old_issues.borrowernumber) <> 0";
2880 my $sth = $dbh->prepare($query);
2881 $sth->execute(@bind_params);
2882 my $rows_affected = $sth->rows; ### doublecheck row count return function
2883 return $rows_affected;
2886 =head2 SendCirculationAlert
2888 Send out a C<check-in> or C<checkout> alert using the messaging system.
2890 B<Parameters>:
2892 =over 4
2894 =item type
2896 Valid values for this parameter are: C<CHECKIN> and C<CHECKOUT>.
2898 =item item
2900 Hashref of information about the item being checked in or out.
2902 =item borrower
2904 Hashref of information about the borrower of the item.
2906 =item branch
2908 The branchcode from where the checkout or check-in took place.
2910 =back
2912 B<Example>:
2914 SendCirculationAlert({
2915 type => 'CHECKOUT',
2916 item => $item,
2917 borrower => $borrower,
2918 branch => $branch,
2921 =cut
2923 sub SendCirculationAlert {
2924 my ($opts) = @_;
2925 my ($type, $item, $borrower, $branch) =
2926 ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch});
2927 my %message_name = (
2928 CHECKIN => 'Item_Check_in',
2929 CHECKOUT => 'Item_Checkout',
2930 RENEWAL => 'Item_Checkout',
2932 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
2933 borrowernumber => $borrower->{borrowernumber},
2934 message_name => $message_name{$type},
2936 my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues';
2937 my $letter = C4::Letters::GetPreparedLetter (
2938 module => 'circulation',
2939 letter_code => $type,
2940 branchcode => $branch,
2941 tables => {
2942 $issues_table => $item->{itemnumber},
2943 'items' => $item->{itemnumber},
2944 'biblio' => $item->{biblionumber},
2945 'biblioitems' => $item->{biblionumber},
2946 'borrowers' => $borrower,
2947 'branches' => $branch,
2949 ) or return;
2951 my @transports = keys %{ $borrower_preferences->{transports} };
2952 # warn "no transports" unless @transports;
2953 for (@transports) {
2954 # warn "transport: $_";
2955 my $message = C4::Message->find_last_message($borrower, $type, $_);
2956 if (!$message) {
2957 #warn "create new message";
2958 C4::Message->enqueue($letter, $borrower, $_);
2959 } else {
2960 #warn "append to old message";
2961 $message->append($letter);
2962 $message->update;
2966 return $letter;
2969 =head2 updateWrongTransfer
2971 $items = updateWrongTransfer($itemNumber,$borrowernumber,$waitingAtLibrary,$FromLibrary);
2973 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
2975 =cut
2977 sub updateWrongTransfer {
2978 my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_;
2979 my $dbh = C4::Context->dbh;
2980 # first step validate the actual line of transfert .
2981 my $sth =
2982 $dbh->prepare(
2983 "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL"
2985 $sth->execute($FromLibrary,$itemNumber);
2986 $sth->finish;
2988 # second step create a new line of branchtransfer to the right location .
2989 ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary);
2991 #third step changing holdingbranch of item
2992 UpdateHoldingbranch($FromLibrary,$itemNumber);
2995 =head2 UpdateHoldingbranch
2997 $items = UpdateHoldingbranch($branch,$itmenumber);
2999 Simple methode for updating hodlingbranch in items BDD line
3001 =cut
3003 sub UpdateHoldingbranch {
3004 my ( $branch,$itemnumber ) = @_;
3005 ModItem({ holdingbranch => $branch }, undef, $itemnumber);
3008 =head2 CalcDateDue
3010 $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower);
3012 this function calculates the due date given the start date and configured circulation rules,
3013 checking against the holidays calendar as per the 'useDaysMode' syspref.
3014 C<$startdate> = C4::Dates object representing start date of loan period (assumed to be today)
3015 C<$itemtype> = itemtype code of item in question
3016 C<$branch> = location whose calendar to use
3017 C<$borrower> = Borrower object
3019 =cut
3021 sub CalcDateDue {
3022 my ( $startdate, $itemtype, $branch, $borrower ) = @_;
3024 # loanlength now a href
3025 my $loanlength =
3026 GetLoanLength( $borrower->{'categorycode'}, $itemtype, $branch );
3028 my $datedue;
3030 # if globalDueDate ON the datedue is set to that date
3031 if (C4::Context->preference('globalDueDate')
3032 && ( C4::Context->preference('globalDueDate') =~
3033 C4::Dates->regexp('syspref') )
3035 $datedue = dt_from_string(
3036 C4::Context->preference('globalDueDate'),
3037 C4::Context->preference('dateformat')
3039 } else {
3041 # otherwise, calculate the datedue as normal
3042 if ( C4::Context->preference('useDaysMode') eq 'Days' )
3043 { # ignoring calendar
3044 my $dt =
3045 DateTime->now( time_zone => C4::Context->tz() )
3046 ->truncate( to => 'minute' );
3047 if ( $loanlength->{lengthunit} eq 'hours' ) {
3048 $dt->add( hours => $loanlength->{issuelength} );
3049 } else { # days
3050 $dt->add( days => $loanlength->{issuelength} );
3051 $dt->set_hour(23);
3052 $dt->set_minute(59);
3054 # break
3055 return $dt;
3057 } else {
3058 my $dur;
3059 if ($loanlength->{lengthunit} eq 'hours') {
3060 $dur = DateTime::Duration->new( hours => $loanlength->{issuelength});
3062 else { # days
3063 $dur = DateTime::Duration->new( days => $loanlength->{issuelength});
3065 if (ref $startdate ne 'DateTime' ) {
3066 $startdate = dt_from_string($startdate);
3068 my $calendar = Koha::Calendar->new( branchcode => $branch );
3069 $datedue = $calendar->addDate( $startdate, $dur, $loanlength->{lengthunit} );
3070 if ($loanlength->{lengthunit} eq 'days') {
3071 $datedue->set_hour(23);
3072 $datedue->set_minute(59);
3077 # if Hard Due Dates are used, retreive them and apply as necessary
3078 my ( $hardduedate, $hardduedatecompare ) =
3079 GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch );
3080 if ($hardduedate) { # hardduedates are currently dates
3081 $hardduedate->truncate( to => 'minute' );
3082 $hardduedate->set_hour(23);
3083 $hardduedate->set_minute(59);
3084 my $cmp = DateTime->compare( $hardduedate, $datedue );
3086 # if the calculated due date is after the 'before' Hard Due Date (ceiling), override
3087 # if the calculated date is before the 'after' Hard Due Date (floor), override
3088 # if the hard due date is set to 'exactly', overrride
3089 if ( $hardduedatecompare == 0 || $hardduedatecompare == $cmp ) {
3090 $datedue = $hardduedate->clone;
3093 # in all other cases, keep the date due as it is
3096 # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate
3097 if ( C4::Context->preference('ReturnBeforeExpiry') ) {
3098 my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso' );
3099 $expiry_dt->set( hour => 23, minute => 59);
3100 if ( DateTime->compare( $datedue, $expiry_dt ) == 1 ) {
3101 $datedue = $expiry_dt->clone;
3105 return $datedue;
3109 =head2 CheckRepeatableHolidays
3111 $countrepeatable = CheckRepeatableHoliday($itemnumber,$week_day,$branchcode);
3113 This function checks if the date due is a repeatable holiday
3115 C<$date_due> = returndate calculate with no day check
3116 C<$itemnumber> = itemnumber
3117 C<$branchcode> = localisation of issue
3119 =cut
3121 sub CheckRepeatableHolidays{
3122 my($itemnumber,$week_day,$branchcode)=@_;
3123 my $dbh = C4::Context->dbh;
3124 my $query = qq|SELECT count(*)
3125 FROM repeatable_holidays
3126 WHERE branchcode=?
3127 AND weekday=?|;
3128 my $sth = $dbh->prepare($query);
3129 $sth->execute($branchcode,$week_day);
3130 my $result=$sth->fetchrow;
3131 $sth->finish;
3132 return $result;
3136 =head2 CheckSpecialHolidays
3138 $countspecial = CheckSpecialHolidays($years,$month,$day,$itemnumber,$branchcode);
3140 This function check if the date is a special holiday
3142 C<$years> = the years of datedue
3143 C<$month> = the month of datedue
3144 C<$day> = the day of datedue
3145 C<$itemnumber> = itemnumber
3146 C<$branchcode> = localisation of issue
3148 =cut
3150 sub CheckSpecialHolidays{
3151 my ($years,$month,$day,$itemnumber,$branchcode) = @_;
3152 my $dbh = C4::Context->dbh;
3153 my $query=qq|SELECT count(*)
3154 FROM `special_holidays`
3155 WHERE year=?
3156 AND month=?
3157 AND day=?
3158 AND branchcode=?
3160 my $sth = $dbh->prepare($query);
3161 $sth->execute($years,$month,$day,$branchcode);
3162 my $countspecial=$sth->fetchrow ;
3163 $sth->finish;
3164 return $countspecial;
3167 =head2 CheckRepeatableSpecialHolidays
3169 $countspecial = CheckRepeatableSpecialHolidays($month,$day,$itemnumber,$branchcode);
3171 This function check if the date is a repeatble special holidays
3173 C<$month> = the month of datedue
3174 C<$day> = the day of datedue
3175 C<$itemnumber> = itemnumber
3176 C<$branchcode> = localisation of issue
3178 =cut
3180 sub CheckRepeatableSpecialHolidays{
3181 my ($month,$day,$itemnumber,$branchcode) = @_;
3182 my $dbh = C4::Context->dbh;
3183 my $query=qq|SELECT count(*)
3184 FROM `repeatable_holidays`
3185 WHERE month=?
3186 AND day=?
3187 AND branchcode=?
3189 my $sth = $dbh->prepare($query);
3190 $sth->execute($month,$day,$branchcode);
3191 my $countspecial=$sth->fetchrow ;
3192 $sth->finish;
3193 return $countspecial;
3198 sub CheckValidBarcode{
3199 my ($barcode) = @_;
3200 my $dbh = C4::Context->dbh;
3201 my $query=qq|SELECT count(*)
3202 FROM items
3203 WHERE barcode=?
3205 my $sth = $dbh->prepare($query);
3206 $sth->execute($barcode);
3207 my $exist=$sth->fetchrow ;
3208 $sth->finish;
3209 return $exist;
3212 =head2 IsBranchTransferAllowed
3214 $allowed = IsBranchTransferAllowed( $toBranch, $fromBranch, $code );
3216 Code is either an itemtype or collection doe depending on the pref BranchTransferLimitsType
3218 =cut
3220 sub IsBranchTransferAllowed {
3221 my ( $toBranch, $fromBranch, $code ) = @_;
3223 if ( $toBranch eq $fromBranch ) { return 1; } ## Short circuit for speed.
3225 my $limitType = C4::Context->preference("BranchTransferLimitsType");
3226 my $dbh = C4::Context->dbh;
3228 my $sth = $dbh->prepare("SELECT * FROM branch_transfer_limits WHERE toBranch = ? AND fromBranch = ? AND $limitType = ?");
3229 $sth->execute( $toBranch, $fromBranch, $code );
3230 my $limit = $sth->fetchrow_hashref();
3232 ## If a row is found, then that combination is not allowed, if no matching row is found, then the combination *is allowed*
3233 if ( $limit->{'limitId'} ) {
3234 return 0;
3235 } else {
3236 return 1;
3240 =head2 CreateBranchTransferLimit
3242 CreateBranchTransferLimit( $toBranch, $fromBranch, $code );
3244 $code is either itemtype or collection code depending on what the pref BranchTransferLimitsType is set to.
3246 =cut
3248 sub CreateBranchTransferLimit {
3249 my ( $toBranch, $fromBranch, $code ) = @_;
3251 my $limitType = C4::Context->preference("BranchTransferLimitsType");
3253 my $dbh = C4::Context->dbh;
3255 my $sth = $dbh->prepare("INSERT INTO branch_transfer_limits ( $limitType, toBranch, fromBranch ) VALUES ( ?, ?, ? )");
3256 $sth->execute( $code, $toBranch, $fromBranch );
3259 =head2 DeleteBranchTransferLimits
3261 DeleteBranchTransferLimits($frombranch);
3263 Deletes all the branch transfer limits for one branch
3265 =cut
3267 sub DeleteBranchTransferLimits {
3268 my $branch = shift;
3269 my $dbh = C4::Context->dbh;
3270 my $sth = $dbh->prepare("DELETE FROM branch_transfer_limits WHERE fromBranch = ?");
3271 $sth->execute($branch);
3274 sub ReturnLostItem{
3275 my ( $borrowernumber, $itemnum ) = @_;
3277 MarkIssueReturned( $borrowernumber, $itemnum );
3278 my $borrower = C4::Members::GetMember( 'borrowernumber'=>$borrowernumber );
3279 my $item = C4::Items::GetItem( $itemnum );
3280 my $old_note = ($item->{'paidfor'} && ($item->{'paidfor'} ne q{})) ? $item->{'paidfor'}.' / ' : q{};
3281 my @datearr = localtime(time);
3282 my $date = ( 1900 + $datearr[5] ) . "-" . ( $datearr[4] + 1 ) . "-" . $datearr[3];
3283 my $bor = "$borrower->{'firstname'} $borrower->{'surname'} $borrower->{'cardnumber'}";
3284 ModItem({ paidfor => $old_note."Paid for by $bor $date" }, undef, $itemnum);
3288 sub LostItem{
3289 my ($itemnumber, $mark_returned, $charge_fee) = @_;
3291 my $dbh = C4::Context->dbh();
3292 my $sth=$dbh->prepare("SELECT issues.*,items.*,biblio.title
3293 FROM issues
3294 JOIN items USING (itemnumber)
3295 JOIN biblio USING (biblionumber)
3296 WHERE issues.itemnumber=?");
3297 $sth->execute($itemnumber);
3298 my $issues=$sth->fetchrow_hashref();
3299 $sth->finish;
3301 # if a borrower lost the item, add a replacement cost to the their record
3302 if ( my $borrowernumber = $issues->{borrowernumber} ){
3303 my $borrower = C4::Members::GetMemberDetails( $borrowernumber );
3305 C4::Accounts::chargelostitem($borrowernumber, $itemnumber, $issues->{'replacementprice'}, "Lost Item $issues->{'title'} $issues->{'barcode'}")
3306 if $charge_fee;
3307 #FIXME : Should probably have a way to distinguish this from an item that really was returned.
3308 #warn " $issues->{'borrowernumber'} / $itemnumber ";
3309 MarkIssueReturned($borrowernumber,$itemnumber,undef,undef,$borrower->{'privacy'}) if $mark_returned;
3313 sub GetOfflineOperations {
3314 my $dbh = C4::Context->dbh;
3315 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE branchcode=? ORDER BY timestamp");
3316 $sth->execute(C4::Context->userenv->{'branch'});
3317 my $results = $sth->fetchall_arrayref({});
3318 $sth->finish;
3319 return $results;
3322 sub GetOfflineOperation {
3323 my $dbh = C4::Context->dbh;
3324 my $sth = $dbh->prepare("SELECT * FROM pending_offline_operations WHERE operationid=?");
3325 $sth->execute( shift );
3326 my $result = $sth->fetchrow_hashref;
3327 $sth->finish;
3328 return $result;
3331 sub AddOfflineOperation {
3332 my $dbh = C4::Context->dbh;
3333 my $sth = $dbh->prepare("INSERT INTO pending_offline_operations (userid, branchcode, timestamp, action, barcode, cardnumber) VALUES(?,?,?,?,?,?)");
3334 $sth->execute( @_ );
3335 return "Added.";
3338 sub DeleteOfflineOperation {
3339 my $dbh = C4::Context->dbh;
3340 my $sth = $dbh->prepare("DELETE FROM pending_offline_operations WHERE operationid=?");
3341 $sth->execute( shift );
3342 return "Deleted.";
3345 sub ProcessOfflineOperation {
3346 my $operation = shift;
3348 my $report;
3349 if ( $operation->{action} eq 'return' ) {
3350 $report = ProcessOfflineReturn( $operation );
3351 } elsif ( $operation->{action} eq 'issue' ) {
3352 $report = ProcessOfflineIssue( $operation );
3355 DeleteOfflineOperation( $operation->{operationid} ) if $operation->{operationid};
3357 return $report;
3360 sub ProcessOfflineReturn {
3361 my $operation = shift;
3363 my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3365 if ( $itemnumber ) {
3366 my $issue = GetOpenIssue( $itemnumber );
3367 if ( $issue ) {
3368 MarkIssueReturned(
3369 $issue->{borrowernumber},
3370 $itemnumber,
3371 undef,
3372 $operation->{timestamp},
3374 ModItem(
3375 { renewals => 0, onloan => undef },
3376 $issue->{'biblionumber'},
3377 $itemnumber
3379 return "Success.";
3380 } else {
3381 return "Item not issued.";
3383 } else {
3384 return "Item not found.";
3388 sub ProcessOfflineIssue {
3389 my $operation = shift;
3391 my $borrower = C4::Members::GetMemberDetails( undef, $operation->{cardnumber} ); # Get borrower from operation cardnumber
3393 if ( $borrower->{borrowernumber} ) {
3394 my $itemnumber = C4::Items::GetItemnumberFromBarcode( $operation->{barcode} );
3395 unless ($itemnumber) {
3396 return "Barcode not found.";
3398 my $issue = GetOpenIssue( $itemnumber );
3400 if ( $issue and ( $issue->{borrowernumber} ne $borrower->{borrowernumber} ) ) { # Item already issued to another borrower, mark it returned
3401 MarkIssueReturned(
3402 $issue->{borrowernumber},
3403 $itemnumber,
3404 undef,
3405 $operation->{timestamp},
3408 AddIssue(
3409 $borrower,
3410 $operation->{'barcode'},
3411 undef,
3413 $operation->{timestamp},
3414 undef,
3416 return "Success.";
3417 } else {
3418 return "Borrower not found.";
3424 =head2 TransferSlip
3426 TransferSlip($user_branch, $itemnumber, $to_branch)
3428 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
3430 =cut
3432 sub TransferSlip {
3433 my ($branch, $itemnumber, $to_branch) = @_;
3435 my $item = GetItem( $itemnumber )
3436 or return;
3438 my $pulldate = C4::Dates->new();
3440 return C4::Letters::GetPreparedLetter (
3441 module => 'circulation',
3442 letter_code => 'TRANSFERSLIP',
3443 branchcode => $branch,
3444 tables => {
3445 'branches' => $to_branch,
3446 'biblio' => $item->{biblionumber},
3447 'items' => $item,
3452 =head2 CheckIfIssuedToPatron
3454 CheckIfIssuedToPatron($borrowernumber, $biblionumber)
3456 Return 1 if any record item is issued to patron, otherwise return 0
3458 =cut
3460 sub CheckIfIssuedToPatron {
3461 my ($borrowernumber, $biblionumber) = @_;
3463 my $items = GetItemsByBiblioitemnumber($biblionumber);
3465 foreach my $item (@{$items}) {
3466 return 1 if ($item->{borrowernumber} && $item->{borrowernumber} eq $borrowernumber);
3469 return;
3475 __END__
3477 =head1 AUTHOR
3479 Koha Development Team <http://koha-community.org/>
3481 =cut