X-Git-Url: http://git.rot13.org/?a=blobdiff_plain;f=C4%2FCirculation.pm;h=40fdcfdc5e89c495f3e42ef13259182847b701cb;hb=babc6fbfe9e3ea8ac960609538dfb45da2e79ed9;hp=f24f2f749d61bc891884b6cfd0cc643f895ee7ef;hpb=4c1ae53d861e20051a776f0195f5da1cfa9a24e3;p=koha.git diff --git a/C4/Circulation.pm b/C4/Circulation.pm index f24f2f749d..40fdcfdc5e 100644 --- a/C4/Circulation.pm +++ b/C4/Circulation.pm @@ -5,53 +5,55 @@ package C4::Circulation; # # This file is part of Koha. # -# Koha is free software; you can redistribute it and/or modify it under the -# terms of the GNU General Public License as published by the Free Software -# Foundation; either version 2 of the License, or (at your option) any later -# version. +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. # -# Koha is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. # -# You should have received a copy of the GNU General Public License along -# with Koha; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . use strict; #use warnings; FIXME - Bug 2505 use DateTime; +use Koha::DateUtils; use C4::Context; use C4::Stats; use C4::Reserves; use C4::Biblio; use C4::Items; use C4::Members; -use C4::Dates; -use C4::Dates qw(format_date); use C4::Accounts; use C4::ItemCirculationAlertPreference; use C4::Message; use C4::Debug; -use C4::Branch; # GetBranches use C4::Log; # logaction -use C4::Koha qw( - GetAuthorisedValueByCode - GetAuthValCode - GetKohaAuthorisedValueLib -); -use C4::Overdues qw(CalcFine UpdateFine); +use C4::Overdues qw(CalcFine UpdateFine get_chargeable_units); use C4::RotatingCollections qw(GetCollectionItemBranches); use Algorithm::CheckDigits; use Data::Dumper; +use Koha::Account; +use Koha::AuthorisedValues; use Koha::DateUtils; use Koha::Calendar; -use Koha::Borrower::Debarments; +use Koha::Items; +use Koha::Patrons; +use Koha::Patron::Debarments; use Koha::Database; +use Koha::Libraries; +use Koha::Holds; +use Koha::RefundLostItemFeeRule; +use Koha::RefundLostItemFeeRules; use Carp; use List::MoreUtils qw( uniq ); +use Scalar::Util qw( looks_like_number ); use Date::Calc qw( Today Today_and_Now @@ -61,11 +63,10 @@ use Date::Calc qw( Day_of_Week Add_Delta_Days ); -use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); +use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); BEGIN { require Exporter; - $VERSION = 3.07.00.049; # for version checking @ISA = qw(Exporter); # FIXME subs that should probably be elsewhere @@ -73,6 +74,7 @@ BEGIN { &barcodedecode &LostItem &ReturnLostItem + &GetPendingOnSiteCheckouts ); # subs to deal with issuing a book @@ -94,6 +96,7 @@ BEGIN { &AnonymiseIssueHistory &CheckIfIssuedToPatron &IsItemIssued + GetTopIssues ); # subs to deal with returns @@ -137,7 +140,7 @@ use C4::Circulation; The functions in this module deal with circulation, issues, and returns, as well as general information about the library. -Also deals with stocktaking. +Also deals with inventory. =head1 FUNCTIONS @@ -163,7 +166,7 @@ System Pref options. # sub barcodedecode { my ($barcode, $filter) = @_; - my $branch = C4::Branch::mybranch(); + my $branch = C4::Context::mybranch(); $filter = C4::Context->preference('itemBarcodeInputFilter') unless $filter; $filter or return $barcode; # ensure filter is defined, else return untouched barcode if ($filter eq 'whitespace') { @@ -303,7 +306,6 @@ sub transferbook { my ( $tbr, $barcode, $ignoreRs ) = @_; my $messages; my $dotransfer = 1; - my $branches = GetBranches(); my $itemnumber = GetItemnumberFromBarcode( $barcode ); my $issue = GetItemIssue($itemnumber); my $biblio = GetBiblioFromItemNumber($itemnumber); @@ -332,7 +334,10 @@ sub transferbook { } # if is permanent... - if ( $hbr && $branches->{$hbr}->{'PE'} ) { + # FIXME Is this still used by someone? + # See other FIXME in AddReturn + my $library = Koha::Libraries->find($hbr); + if ( $library and $library->get_categories->search({'me.categorycode' => 'PE'})->count ) { $messages->{'IsPermanent'} = $hbr; $dotransfer = 0; } @@ -377,6 +382,9 @@ sub TooMany { my $borrower = shift; my $biblionumber = shift; my $item = shift; + my $params = shift; + my $onsite_checkout = $params->{onsite_checkout} || 0; + my $switch_onsite_checkout = $params->{switch_onsite_checkout} || 0; my $cat_borrower = $borrower->{'categorycode'}; my $dbh = C4::Context->dbh; my $branch; @@ -395,8 +403,11 @@ sub TooMany { # rule if (defined($issuing_rule) and defined($issuing_rule->{'maxissueqty'})) { my @bind_params; - my $count_query = "SELECT COUNT(*) FROM issues - JOIN items USING (itemnumber) "; + my $count_query = q| + SELECT COUNT(*) AS total, COALESCE(SUM(onsite_checkout), 0) AS onsite_checkouts + FROM issues + JOIN items USING (itemnumber) + |; my $rule_itemtype = $issuing_rule->{itemtype}; if ($rule_itemtype eq "*") { @@ -449,13 +460,37 @@ sub TooMany { } } - my $count_sth = $dbh->prepare($count_query); - $count_sth->execute(@bind_params); - my ($current_loan_count) = $count_sth->fetchrow_array; + my ( $checkout_count, $onsite_checkout_count ) = $dbh->selectrow_array( $count_query, {}, @bind_params ); + + my $max_checkouts_allowed = $issuing_rule->{maxissueqty}; + my $max_onsite_checkouts_allowed = $issuing_rule->{maxonsiteissueqty}; - my $max_loans_allowed = $issuing_rule->{'maxissueqty'}; - if ($current_loan_count >= $max_loans_allowed) { - return ($current_loan_count, $max_loans_allowed); + if ( $onsite_checkout ) { + if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed ) { + return { + reason => 'TOO_MANY_ONSITE_CHECKOUTS', + count => $onsite_checkout_count, + max_allowed => $max_onsite_checkouts_allowed, + } + } + } + if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) { + my $delta = $switch_onsite_checkout ? 1 : 0; + if ( $checkout_count >= $max_checkouts_allowed + $delta ) { + return { + reason => 'TOO_MANY_CHECKOUTS', + count => $checkout_count, + max_allowed => $max_checkouts_allowed, + }; + } + } elsif ( not $onsite_checkout ) { + if ( $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed ) { + return { + reason => 'TOO_MANY_CHECKOUTS', + count => $checkout_count - $onsite_checkout_count, + max_allowed => $max_checkouts_allowed, + }; + } } } @@ -463,9 +498,12 @@ sub TooMany { my $branch_borrower_circ_rule = GetBranchBorrowerCircRule($branch, $cat_borrower); if (defined($branch_borrower_circ_rule->{maxissueqty})) { my @bind_params = (); - my $branch_count_query = "SELECT COUNT(*) FROM issues - JOIN items USING (itemnumber) - WHERE borrowernumber = ? "; + my $branch_count_query = q| + SELECT COUNT(*) AS total, COALESCE(SUM(onsite_checkout), 0) AS onsite_checkouts + FROM issues + JOIN items USING (itemnumber) + WHERE borrowernumber = ? + |; push @bind_params, $borrower->{borrowernumber}; if (C4::Context->preference('CircControl') eq 'PickupLibrary') { @@ -477,13 +515,36 @@ sub TooMany { $branch_count_query .= " AND items.homebranch = ? "; push @bind_params, $branch; } - my $branch_count_sth = $dbh->prepare($branch_count_query); - $branch_count_sth->execute(@bind_params); - my ($current_loan_count) = $branch_count_sth->fetchrow_array; - - my $max_loans_allowed = $branch_borrower_circ_rule->{maxissueqty}; - if ($current_loan_count >= $max_loans_allowed) { - return ($current_loan_count, $max_loans_allowed); + my ( $checkout_count, $onsite_checkout_count ) = $dbh->selectrow_array( $branch_count_query, {}, @bind_params ); + my $max_checkouts_allowed = $branch_borrower_circ_rule->{maxissueqty}; + my $max_onsite_checkouts_allowed = $branch_borrower_circ_rule->{maxonsiteissueqty}; + + if ( $onsite_checkout ) { + if ( $onsite_checkout_count >= $max_onsite_checkouts_allowed ) { + return { + reason => 'TOO_MANY_ONSITE_CHECKOUTS', + count => $onsite_checkout_count, + max_allowed => $max_onsite_checkouts_allowed, + } + } + } + if ( C4::Context->preference('ConsiderOnSiteCheckoutsAsNormalCheckouts') ) { + my $delta = $switch_onsite_checkout ? 1 : 0; + if ( $checkout_count >= $max_checkouts_allowed + $delta ) { + return { + reason => 'TOO_MANY_CHECKOUTS', + count => $checkout_count, + max_allowed => $max_checkouts_allowed, + }; + } + } elsif ( not $onsite_checkout ) { + if ( $checkout_count - $onsite_checkout_count >= $max_checkouts_allowed ) { + return { + reason => 'TOO_MANY_CHECKOUTS', + count => $checkout_count - $onsite_checkout_count, + max_allowed => $max_checkouts_allowed, + }; + } } } @@ -491,118 +552,10 @@ sub TooMany { return; } -=head2 itemissues - - @issues = &itemissues($biblioitemnumber, $biblio); - -Looks up information about who has borrowed the bookZ<>(s) with the -given biblioitemnumber. - -C<$biblio> is ignored. - -C<&itemissues> returns an array of references-to-hash. The keys -include the fields from the C table in the Koha database. -Additional keys include: - -=over 4 - -=item C - -If the item is currently on loan, this gives the due date. - -If the item is not on loan, then this is either "Available" or -"Cancelled", if the item has been withdrawn. - -=item C - -If the item is currently on loan, this gives the card number of the -patron who currently has the item. - -=item C, C, C - -These give the timestamp for the last three times the item was -borrowed. - -=item C, C, C - -The card number of the last three patrons who borrowed this item. - -=item C, C, C - -The borrower number of the last three patrons who borrowed this item. - -=back - -=cut - -#' -sub itemissues { - my ( $bibitem, $biblio ) = @_; - my $dbh = C4::Context->dbh; - my $sth = - $dbh->prepare("Select * from items where items.biblioitemnumber = ?") - || die $dbh->errstr; - my $i = 0; - my @results; - - $sth->execute($bibitem) || die $sth->errstr; - - while ( my $data = $sth->fetchrow_hashref ) { - - # Find out who currently has this item. - # FIXME - Wouldn't it be better to do this as a left join of - # some sort? Currently, this code assumes that if - # fetchrow_hashref() fails, then the book is on the shelf. - # fetchrow_hashref() can fail for any number of reasons (e.g., - # database server crash), not just because no items match the - # search criteria. - my $sth2 = $dbh->prepare( - "SELECT * FROM issues - LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber - WHERE itemnumber = ? - " - ); - - $sth2->execute( $data->{'itemnumber'} ); - if ( my $data2 = $sth2->fetchrow_hashref ) { - $data->{'date_due'} = $data2->{'date_due'}; - $data->{'card'} = $data2->{'cardnumber'}; - $data->{'borrower'} = $data2->{'borrowernumber'}; - } - else { - $data->{'date_due'} = ($data->{'withdrawn'} eq '1') ? 'Cancelled' : 'Available'; - } - - - # Find the last 3 people who borrowed this item. - $sth2 = $dbh->prepare( - "SELECT * FROM old_issues - LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber - WHERE itemnumber = ? - ORDER BY returndate DESC,timestamp DESC" - ); - - $sth2->execute( $data->{'itemnumber'} ); - for ( my $i2 = 0 ; $i2 < 2 ; $i2++ ) - { # FIXME : error if there is less than 3 pple borrowing this item - if ( my $data2 = $sth2->fetchrow_hashref ) { - $data->{"timestamp$i2"} = $data2->{'timestamp'}; - $data->{"card$i2"} = $data2->{'cardnumber'}; - $data->{"borrower$i2"} = $data2->{'borrowernumber'}; - } # if - } # for - - $results[$i] = $data; - $i++; - } - - return (@results); -} - =head2 CanBookBeIssued ( $issuingimpossible, $needsconfirmation ) = CanBookBeIssued( $borrower, - $barcode, $duedatespec, $inprocess, $ignore_reserves ); + $barcode, $duedate, $inprocess, $ignore_reserves, $params ); Check if a book can be issued. @@ -614,11 +567,18 @@ C<$issuingimpossible> and C<$needsconfirmation> are some hashref. =item C<$barcode> is the bar code of the book being issued. -=item C<$duedatespec> is a C4::Dates object. +=item C<$duedates> is a DateTime object. =item C<$inprocess> boolean switch + =item C<$ignore_reserves> boolean switch +=item C<$params> Hashref of additional parameters + +Available keys: + override_high_holds - Ignore high holds + onsite_checkout - Checkout is an onsite checkout that will not leave the library + =back Returns : @@ -694,10 +654,14 @@ if the borrower borrows to much things =cut sub CanBookBeIssued { - my ( $borrower, $barcode, $duedate, $inprocess, $ignore_reserves ) = @_; + my ( $borrower, $barcode, $duedate, $inprocess, $ignore_reserves, $params ) = @_; my %needsconfirmation; # filled with problems that needs confirmations my %issuingimpossible; # filled with problems that causes the issue to be IMPOSSIBLE my %alerts; # filled with messages that shouldn't stop issuing, but the librarian should be aware of. + my %messages; # filled with information messages that should be displayed. + + my $onsite_checkout = $params->{onsite_checkout} || 0; + my $override_high_holds = $params->{override_high_holds} || 0; my $item = GetItem(GetItemnumberFromBarcode( $barcode )); my $issue = GetItemIssue($item->{itemnumber}); @@ -747,43 +711,36 @@ sub CanBookBeIssued { branch => C4::Context->userenv->{'branch'}, type => 'localuse', itemnumber => $item->{'itemnumber'}, - itemtype => $item->{'itemtype'}, + itemtype => $item->{'itype'}, borrowernumber => $borrower->{'borrowernumber'}, ccode => $item->{'ccode'}} ); ModDateLastSeen( $item->{'itemnumber'} ); return( { STATS => 1 }, {}); } - if ( $borrower->{flags}->{GNA} ) { - $issuingimpossible{GNA} = 1; - } - if ( $borrower->{flags}->{'LOST'} ) { - $issuingimpossible{CARD_LOST} = 1; - } - if ( $borrower->{flags}->{'DBARRED'} ) { - $issuingimpossible{DEBARRED} = 1; + if ( ref $borrower->{flags} ) { + if ( $borrower->{flags}->{GNA} ) { + $issuingimpossible{GNA} = 1; + } + if ( $borrower->{flags}->{'LOST'} ) { + $issuingimpossible{CARD_LOST} = 1; + } + if ( $borrower->{flags}->{'DBARRED'} ) { + $issuingimpossible{DEBARRED} = 1; + } } if ( !defined $borrower->{dateexpiry} || $borrower->{'dateexpiry'} eq '0000-00-00') { $issuingimpossible{EXPIRED} = 1; } else { - my ($y, $m, $d) = split /-/,$borrower->{'dateexpiry'}; - if ($y && $m && $d) { # are we really writing oinvalid dates to borrs - my $expiry_dt = DateTime->new( - year => $y, - month => $m, - day => $d, - time_zone => C4::Context->tz, - ); - $expiry_dt->truncate( to => 'day'); - my $today = $now->clone()->truncate(to => 'day'); - if (DateTime->compare($today, $expiry_dt) == 1) { - $issuingimpossible{EXPIRED} = 1; - } - } else { - carp("Invalid expity date in borr"); + my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'sql', 'floating' ); + $expiry_dt->truncate( to => 'day'); + my $today = $now->clone()->truncate(to => 'day'); + $today->set_time_zone( 'floating' ); + if ( DateTime->compare($today, $expiry_dt) == 1 ) { $issuingimpossible{EXPIRED} = 1; } } + # # BORROWER STATUS # @@ -791,9 +748,32 @@ sub CanBookBeIssued { # DEBTS my ($balance, $non_issue_charges, $other_charges) = C4::Members::GetMemberAccountBalance( $borrower->{'borrowernumber'} ); + my $amountlimit = C4::Context->preference("noissuescharge"); my $allowfineoverride = C4::Context->preference("AllowFineOverride"); my $allfinesneedoverride = C4::Context->preference("AllFinesNeedOverride"); + + # Check the debt of this patrons guarantees + my $no_issues_charge_guarantees = C4::Context->preference("NoIssuesChargeGuarantees"); + $no_issues_charge_guarantees = undef unless looks_like_number( $no_issues_charge_guarantees ); + if ( defined $no_issues_charge_guarantees ) { + my $p = Koha::Patrons->find( $borrower->{borrowernumber} ); + my @guarantees = $p->guarantees(); + my $guarantees_non_issues_charges; + foreach my $g ( @guarantees ) { + my ( $b, $n, $o ) = C4::Members::GetMemberAccountBalance( $g->id ); + $guarantees_non_issues_charges += $n; + } + + if ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && !$allowfineoverride) { + $issuingimpossible{DEBT_GUARANTEES} = $guarantees_non_issues_charges; + } elsif ( $guarantees_non_issues_charges > $no_issues_charge_guarantees && !$inprocess && $allowfineoverride) { + $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges; + } elsif ( $allfinesneedoverride && $guarantees_non_issues_charges > 0 && $guarantees_non_issues_charges <= $no_issues_charge_guarantees && !$inprocess ) { + $needsconfirmation{DEBT_GUARANTEES} = $guarantees_non_issues_charges; + } + } + if ( C4::Context->preference("IssuingInProcess") ) { if ( $non_issue_charges > $amountlimit && !$inprocess && !$allowfineoverride) { $issuingimpossible{DEBT} = sprintf( "%.2f", $non_issue_charges ); @@ -812,50 +792,62 @@ sub CanBookBeIssued { $needsconfirmation{DEBT} = sprintf( "%.2f", $non_issue_charges ); } } + if ($balance > 0 && $other_charges > 0) { $alerts{OTHER_CHARGES} = sprintf( "%.2f", $other_charges ); } - my ($blocktype, $count) = C4::Members::IsMemberBlocked($borrower->{'borrowernumber'}); - if ($blocktype == -1) { - ## patron has outstanding overdue loans - if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){ - $issuingimpossible{USERBLOCKEDOVERDUE} = $count; - } - elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){ - $needsconfirmation{USERBLOCKEDOVERDUE} = $count; - } - } elsif($blocktype == 1) { - # patron has accrued fine days or has a restriction. $count is a date - if ($count eq '9999-12-31') { - $issuingimpossible{USERBLOCKEDNOENDDATE} = $count; + my $patron = Koha::Patrons->find( $borrower->{borrowernumber} ); + if ( my $debarred_date = $patron->is_debarred ) { + # patron has accrued fine days or has a restriction. $count is a date + if ($debarred_date eq '9999-12-31') { + $issuingimpossible{USERBLOCKEDNOENDDATE} = $debarred_date; } else { - $issuingimpossible{USERBLOCKEDWITHENDDATE} = $count; + $issuingimpossible{USERBLOCKEDWITHENDDATE} = $debarred_date; + } + } elsif ( my $num_overdues = $patron->has_overdues ) { + ## patron has outstanding overdue loans + if ( C4::Context->preference("OverduesBlockCirc") eq 'block'){ + $issuingimpossible{USERBLOCKEDOVERDUE} = $num_overdues; + } + elsif ( C4::Context->preference("OverduesBlockCirc") eq 'confirmation'){ + $needsconfirmation{USERBLOCKEDOVERDUE} = $num_overdues; } } -# - # JB34 CHECKS IF BORROWERS DONT HAVE ISSUE TOO MANY BOOKS + # JB34 CHECKS IF BORROWERS DON'T HAVE ISSUE TOO MANY BOOKS # - my ($current_loan_count, $max_loans_allowed) = TooMany( $borrower, $item->{biblionumber}, $item ); - # if TooMany max_loans_allowed returns 0 the user doesn't have permission to check out this book - if (defined $max_loans_allowed && $max_loans_allowed == 0) { - $needsconfirmation{PATRON_CANT} = 1; - } else { - if($max_loans_allowed){ - if ( C4::Context->preference("AllowTooManyOverride") ) { - $needsconfirmation{TOO_MANY} = 1; - $needsconfirmation{current_loan_count} = $current_loan_count; - $needsconfirmation{max_loans_allowed} = $max_loans_allowed; - } else { - $issuingimpossible{TOO_MANY} = 1; - $issuingimpossible{current_loan_count} = $current_loan_count; - $issuingimpossible{max_loans_allowed} = $max_loans_allowed; - } + my $switch_onsite_checkout = + C4::Context->preference('SwitchOnSiteCheckouts') + and $issue->{onsite_checkout} + and $issue + and $issue->{borrowernumber} == $borrower->{'borrowernumber'} ? 1 : 0; + my $toomany = TooMany( $borrower, $item->{biblionumber}, $item, { onsite_checkout => $onsite_checkout, switch_onsite_checkout => $switch_onsite_checkout, } ); + # if TooMany max_allowed returns 0 the user doesn't have permission to check out this book + if ( $toomany ) { + if ( $toomany->{max_allowed} == 0 ) { + $needsconfirmation{PATRON_CANT} = 1; + } + if ( C4::Context->preference("AllowTooManyOverride") ) { + $needsconfirmation{TOO_MANY} = $toomany->{reason}; + $needsconfirmation{current_loan_count} = $toomany->{count}; + $needsconfirmation{max_loans_allowed} = $toomany->{max_allowed}; + } else { + $issuingimpossible{TOO_MANY} = $toomany->{reason}; + $issuingimpossible{current_loan_count} = $toomany->{count}; + $issuingimpossible{max_loans_allowed} = $toomany->{max_allowed}; } } + # + # CHECKPREVCHECKOUT: CHECK IF ITEM HAS EVER BEEN LENT TO PATRON + # + $patron = Koha::Patrons->find($borrower->{borrowernumber}); + my $wants_check = $patron->wants_check_for_previous_checkout; + $needsconfirmation{PREVISSUE} = 1 + if ($wants_check and $patron->do_check_for_previous_checkout($item)); + # # ITEM CHECKING # @@ -906,7 +898,8 @@ sub CanBookBeIssued { $issuingimpossible{RESTRICTED} = 1; } if ( $item->{'itemlost'} && C4::Context->preference("IssueLostItem") ne 'nothing' ) { - my $code = GetAuthorisedValueByCode( 'LOST', $item->{'itemlost'} ); + my $av = Koha::AuthorisedValues->search({ category => 'LOST', authorised_value => $item->{itemlost} }); + my $code = $av->count ? $av->next->lib : ''; $needsconfirmation{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'confirm' ); $alerts{ITEM_LOST} = $code if ( C4::Context->preference("IssueLostItem") eq 'alert' ); } @@ -917,7 +910,7 @@ sub CanBookBeIssued { $issuingimpossible{ITEMNOTSAMEBRANCH} = 1; $issuingimpossible{'itemhomebranch'} = $item->{C4::Context->preference("HomeOrHoldingBranch")}; } - $needsconfirmation{BORRNOTSAMEBRANCH} = GetBranchName( $borrower->{'branchcode'} ) + $needsconfirmation{BORRNOTSAMEBRANCH} = $borrower->{'branchcode'} if ( $borrower->{'branchcode'} ne $userenv->{branch} ); } } @@ -928,7 +921,7 @@ sub CanBookBeIssued { if ( $rentalConfirmation ){ my ($rentalCharge) = GetIssuingCharges( $item->{'itemnumber'}, $borrower->{'borrowernumber'} ); - if ( $rentalCharge ){ + if ( $rentalCharge > 0 ){ $rentalCharge = sprintf("%.02f", $rentalCharge); $needsconfirmation{RENTALCHARGE} = $rentalCharge; } @@ -939,17 +932,29 @@ sub CanBookBeIssued { # if ( $issue->{borrowernumber} && $issue->{borrowernumber} eq $borrower->{'borrowernumber'} ){ - # Already issued to current borrower. Ask whether the loan should - # be renewed. - my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed( - $borrower->{'borrowernumber'}, - $item->{'itemnumber'} - ); - if ( $CanBookBeRenewed == 0 ) { # no more renewals allowed - $issuingimpossible{NO_MORE_RENEWALS} = 1; - } - else { - $needsconfirmation{RENEW_ISSUE} = 1; + # Already issued to current borrower. + # If it is an on-site checkout if it can be switched to a normal checkout + # or ask whether the loan should be renewed + + if ( $issue->{onsite_checkout} + and C4::Context->preference('SwitchOnSiteCheckouts') ) { + $messages{ONSITE_CHECKOUT_WILL_BE_SWITCHED} = 1; + } else { + my ($CanBookBeRenewed,$renewerror) = CanBookBeRenewed( + $borrower->{'borrowernumber'}, + $item->{'itemnumber'}, + ); + if ( $CanBookBeRenewed == 0 ) { # no more renewals allowed + if ( $renewerror eq 'onsite_checkout' ) { + $issuingimpossible{NO_RENEWAL_FOR_ONSITE_CHECKOUTS} = 1; + } + else { + $issuingimpossible{NO_MORE_RENEWALS} = 1; + } + } + else { + $needsconfirmation{RENEW_ISSUE} = 1; + } } } elsif ($issue->{borrowernumber}) { @@ -957,12 +962,19 @@ sub CanBookBeIssued { # issued to someone else my $currborinfo = C4::Members::GetMember( borrowernumber => $issue->{borrowernumber} ); -# warn "=>.$currborinfo->{'firstname'} $currborinfo->{'surname'} ($currborinfo->{'cardnumber'})"; - $needsconfirmation{ISSUED_TO_ANOTHER} = 1; - $needsconfirmation{issued_firstname} = $currborinfo->{'firstname'}; - $needsconfirmation{issued_surname} = $currborinfo->{'surname'}; - $needsconfirmation{issued_cardnumber} = $currborinfo->{'cardnumber'}; - $needsconfirmation{issued_borrowernumber} = $currborinfo->{'borrowernumber'}; + + my ( $can_be_returned, $message ) = CanBookBeReturned( $item, C4::Context->userenv->{branch} ); + + unless ( $can_be_returned ) { + $issuingimpossible{RETURN_IMPOSSIBLE} = 1; + $issuingimpossible{branch_to_return} = $message; + } else { + $needsconfirmation{ISSUED_TO_ANOTHER} = 1; + $needsconfirmation{issued_firstname} = $currborinfo->{'firstname'}; + $needsconfirmation{issued_surname} = $currborinfo->{'surname'}; + $needsconfirmation{issued_cardnumber} = $currborinfo->{'cardnumber'}; + $needsconfirmation{issued_borrowernumber} = $currborinfo->{'borrowernumber'}; + } } unless ( $ignore_reserves ) { @@ -972,7 +984,6 @@ sub CanBookBeIssued { my $resbor = $res->{'borrowernumber'}; if ( $resbor ne $borrower->{'borrowernumber'} ) { my ( $resborrower ) = C4::Members::GetMember( borrowernumber => $resbor ); - my $branchname = GetBranchName( $res->{'branchcode'} ); if ( $restype eq "Waiting" ) { # The item is on reserve and waiting, but has been @@ -982,8 +993,8 @@ sub CanBookBeIssued { $needsconfirmation{'ressurname'} = $resborrower->{'surname'}; $needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'}; $needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'}; - $needsconfirmation{'resbranchname'} = $branchname; - $needsconfirmation{'reswaitingdate'} = format_date($res->{'waitingdate'}); + $needsconfirmation{'resbranchcode'} = $res->{branchcode}; + $needsconfirmation{'reswaitingdate'} = $res->{'waitingdate'}; } elsif ( $restype eq "Reserved" ) { # The item is on reserve for someone else. @@ -992,8 +1003,8 @@ sub CanBookBeIssued { $needsconfirmation{'ressurname'} = $resborrower->{'surname'}; $needsconfirmation{'rescardnumber'} = $resborrower->{'cardnumber'}; $needsconfirmation{'resborrowernumber'} = $resborrower->{'borrowernumber'}; - $needsconfirmation{'resbranchname'} = $branchname; - $needsconfirmation{'resreservedate'} = format_date($res->{'reservedate'}); + $needsconfirmation{'resbranchcode'} = $res->{branchcode}; + $needsconfirmation{'resreservedate'} = $res->{'reservedate'}; } } } @@ -1012,17 +1023,24 @@ sub CanBookBeIssued { } ## check for high holds decreasing loan period - my $decrease_loan = C4::Context->preference('decreaseLoanHighHolds'); - if ( $decrease_loan && $decrease_loan == 1 ) { - my ( $reserved, $num, $duration, $returndate ) = - checkHighHolds( $item, $borrower ); - - if ( $num >= C4::Context->preference('decreaseLoanHighHoldsValue') ) { - $needsconfirmation{HIGHHOLDS} = { - num_holds => $num, - duration => $duration, - returndate => output_pref($returndate), - }; + if ( C4::Context->preference('decreaseLoanHighHolds') ) { + my $check = checkHighHolds( $item, $borrower ); + + if ( $check->{exceeded} ) { + if ($override_high_holds) { + $alerts{HIGHHOLDS} = { + num_holds => $check->{outstanding}, + duration => $check->{duration}, + returndate => output_pref( $check->{due_date} ), + }; + } + else { + $needsconfirmation{HIGHHOLDS} = { + num_holds => $check->{outstanding}, + duration => $check->{duration}, + returndate => output_pref( $check->{due_date} ), + }; + } } } @@ -1052,7 +1070,7 @@ sub CanBookBeIssued { } } - return ( \%issuingimpossible, \%needsconfirmation, \%alerts ); + return ( \%issuingimpossible, \%needsconfirmation, \%alerts, \%messages, ); } =head2 CanBookBeReturned @@ -1116,13 +1134,60 @@ sub checkHighHolds { my ( $item, $borrower ) = @_; my $biblio = GetBiblioFromItemNumber( $item->{itemnumber} ); my $branch = _GetCircControlBranch( $item, $borrower ); - my $dbh = C4::Context->dbh; - my $sth = $dbh->prepare( -'select count(borrowernumber) as num_holds from reserves where biblionumber=?' - ); - $sth->execute( $item->{'biblionumber'} ); - my ($holds) = $sth->fetchrow_array; - if ($holds) { + + my $return_data = { + exceeded => 0, + outstanding => 0, + duration => 0, + due_date => undef, + }; + + my $holds = Koha::Holds->search( { biblionumber => $item->{'biblionumber'} } ); + + if ( $holds->count() ) { + $return_data->{outstanding} = $holds->count(); + + my $decreaseLoanHighHoldsControl = C4::Context->preference('decreaseLoanHighHoldsControl'); + my $decreaseLoanHighHoldsValue = C4::Context->preference('decreaseLoanHighHoldsValue'); + my $decreaseLoanHighHoldsIgnoreStatuses = C4::Context->preference('decreaseLoanHighHoldsIgnoreStatuses'); + + my @decreaseLoanHighHoldsIgnoreStatuses = split( /,/, $decreaseLoanHighHoldsIgnoreStatuses ); + + if ( $decreaseLoanHighHoldsControl eq 'static' ) { + + # static means just more than a given number of holds on the record + + # If the number of holds is less than the threshold, we can stop here + if ( $holds->count() < $decreaseLoanHighHoldsValue ) { + return $return_data; + } + } + elsif ( $decreaseLoanHighHoldsControl eq 'dynamic' ) { + + # dynamic means X more than the number of holdable items on the record + + # let's get the items + my @items = $holds->next()->biblio()->items(); + + # Remove any items with status defined to be ignored even if the would not make item unholdable + foreach my $status (@decreaseLoanHighHoldsIgnoreStatuses) { + @items = grep { !$_->$status } @items; + } + + # Remove any items that are not holdable for this patron + @items = grep { CanItemBeReserved( $borrower->{borrowernumber}, $_->itemnumber ) eq 'OK' } @items; + + my $items_count = scalar @items; + + my $threshold = $items_count + $decreaseLoanHighHoldsValue; + + # If the number of holds is less than the count of items we have + # plus the number of holds allowed above that count, we can stop here + if ( $holds->count() <= $threshold ) { + return $return_data; + } + } + my $issuedate = DateTime->now( time_zone => C4::Context->tz() ); my $calendar = Koha::Calendar->new( branchcode => $branch ); @@ -1131,21 +1196,21 @@ sub checkHighHolds { ( C4::Context->preference('item-level_itypes') ) ? $biblio->{'itype'} : $biblio->{'itemtype'}; - my $orig_due = - C4::Circulation::CalcDateDue( $issuedate, $itype, $branch, - $borrower ); - my $reduced_datedue = - $calendar->addDate( $issuedate, - C4::Context->preference('decreaseLoanHighHoldsDuration') ); + my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branch, $borrower ); + + my $decreaseLoanHighHoldsDuration = C4::Context->preference('decreaseLoanHighHoldsDuration'); + + my $reduced_datedue = $calendar->addDate( $issuedate, $decreaseLoanHighHoldsDuration ); if ( DateTime->compare( $reduced_datedue, $orig_due ) == -1 ) { - return ( 1, $holds, - C4::Context->preference('decreaseLoanHighHoldsDuration'), - $reduced_datedue ); + $return_data->{exceeded} = 1; + $return_data->{duration} = $decreaseLoanHighHoldsDuration; + $return_data->{due_date} = $reduced_datedue; } } - return ( 0, 0, 0, undef ); + + return $return_data; } =head2 AddIssue @@ -1160,13 +1225,13 @@ Issue a book. Does no check, they are done in CanBookBeIssued. If we reach this =item C<$barcode> is the barcode of the item being issued. -=item C<$datedue> is a C4::Dates object for the max date of return, i.e. the date due (optional). +=item C<$datedue> is a DateTime object for the max date of return, i.e. the date due (optional). Calculated if empty. =item C<$cancelreserve> is 1 to override and cancel any pending reserves for the item (optional). =item C<$issuedate> is the date to issue the item in iso (YYYY-MM-DD) format (optional). -Defaults to today. Unlike C<$datedue>, NOT a C4::Dates object, unfortunately. +Defaults to today. Unlike C<$datedue>, NOT a DateTime object, unfortunately. AddIssue does the following things : @@ -1188,173 +1253,199 @@ AddIssue does the following things : sub AddIssue { my ( $borrower, $barcode, $datedue, $cancelreserve, $issuedate, $sipmode, $params ) = @_; + my $onsite_checkout = $params && $params->{onsite_checkout} ? 1 : 0; + my $switch_onsite_checkout = $params && $params->{switch_onsite_checkout}; my $auto_renew = $params && $params->{auto_renew}; - my $dbh = C4::Context->dbh; - my $barcodecheck=CheckValidBarcode($barcode); + my $dbh = C4::Context->dbh; + my $barcodecheck = CheckValidBarcode($barcode); - if ($datedue && ref $datedue ne 'DateTime') { + my $issue; + + if ( $datedue && ref $datedue ne 'DateTime' ) { $datedue = dt_from_string($datedue); } + # $issuedate defaults to today. - if ( ! defined $issuedate ) { - $issuedate = DateTime->now(time_zone => C4::Context->tz()); + if ( !defined $issuedate ) { + $issuedate = DateTime->now( time_zone => C4::Context->tz() ); } else { - if ( ref $issuedate ne 'DateTime') { + if ( ref $issuedate ne 'DateTime' ) { $issuedate = dt_from_string($issuedate); } } - if ($borrower and $barcode and $barcodecheck ne '0'){#??? wtf - # find which item we issue - my $item = GetItem('', $barcode) or return; # if we don't get an Item, abort. - my $branch = _GetCircControlBranch($item,$borrower); - - # get actual issuing if there is one - my $actualissue = GetItemIssue( $item->{itemnumber}); - - # get biblioinformation for this item - my $biblio = GetBiblioFromItemNumber($item->{itemnumber}); - - # - # check if we just renew the issue. - # - if ($actualissue->{borrowernumber} eq $borrower->{'borrowernumber'}) { - $datedue = AddRenewal( - $borrower->{'borrowernumber'}, - $item->{'itemnumber'}, - $branch, - $datedue, - $issuedate, # here interpreted as the renewal date - ); - } - else { - # it's NOT a renewal - if ( $actualissue->{borrowernumber}) { - # This book is currently on loan, but not to the person - # who wants to borrow it now. mark it returned before issuing to the new borrower - AddReturn( - $item->{'barcode'}, - C4::Context->userenv->{'branch'} - ); - } + + # Stop here if the patron or barcode doesn't exist + if ( $borrower && $barcode && $barcodecheck ) { + # find which item we issue + my $item = GetItem( '', $barcode ) + or return; # if we don't get an Item, abort. + + my $branch = _GetCircControlBranch( $item, $borrower ); + + # get actual issuing if there is one + my $actualissue = GetItemIssue( $item->{itemnumber} ); + + # get biblioinformation for this item + my $biblio = GetBiblioFromItemNumber( $item->{itemnumber} ); + + # check if we just renew the issue. + if ( $actualissue->{borrowernumber} eq $borrower->{'borrowernumber'} + and not $switch_onsite_checkout ) { + $datedue = AddRenewal( + $borrower->{'borrowernumber'}, + $item->{'itemnumber'}, + $branch, + $datedue, + $issuedate, # here interpreted as the renewal date + ); + } + else { + # it's NOT a renewal + if ( $actualissue->{borrowernumber} + and not $switch_onsite_checkout ) { + # This book is currently on loan, but not to the person + # who wants to borrow it now. mark it returned before issuing to the new borrower + my ( $allowed, $message ) = CanBookBeReturned( $item, C4::Context->userenv->{branch} ); + return unless $allowed; + AddReturn( $item->{'barcode'}, C4::Context->userenv->{'branch'} ); + } MoveReserve( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $cancelreserve ); - # Starting process for transfer job (checking transfert and validate it if we have one) - my ($datesent) = GetTransfers($item->{'itemnumber'}); + + # Starting process for transfer job (checking transfert and validate it if we have one) + my ($datesent) = GetTransfers( $item->{'itemnumber'} ); if ($datesent) { - # updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....) - my $sth = - $dbh->prepare( + # updating line of branchtranfert to finish it, and changing the to branch value, implement a comment for visibility of this case (maybe for stats ....) + my $sth = $dbh->prepare( "UPDATE branchtransfers SET datearrived = now(), tobranch = ?, comments = 'Forced branchtransfer' WHERE itemnumber= ? AND datearrived IS NULL" - ); - $sth->execute(C4::Context->userenv->{'branch'},$item->{'itemnumber'}); + ); + $sth->execute( C4::Context->userenv->{'branch'}, + $item->{'itemnumber'} ); } - # If automatic renewal wasn't selected while issuing, set the value according to the issuing rule. - unless ($auto_renew) { - my $issuingrule = GetIssuingRule($borrower->{categorycode}, $item->{itype}, $branch); - $auto_renew = $issuingrule->{auto_renew}; - } - - # Record in the database the fact that the book was issued. - my $sth = - $dbh->prepare( - "INSERT INTO issues - (borrowernumber, itemnumber,issuedate, date_due, branchcode, onsite_checkout, auto_renew) - VALUES (?,?,?,?,?,?,?)" - ); - unless ($datedue) { - my $itype = ( C4::Context->preference('item-level_itypes') ) ? $biblio->{'itype'} : $biblio->{'itemtype'}; - $datedue = CalcDateDue( $issuedate, $itype, $branch, $borrower ); - - } - $datedue->truncate( to => 'minute'); - - $sth->execute( - $borrower->{'borrowernumber'}, # borrowernumber - $item->{'itemnumber'}, # itemnumber - $issuedate->strftime('%Y-%m-%d %H:%M:%S'), # issuedate - $datedue->strftime('%Y-%m-%d %H:%M:%S'), # date_due - C4::Context->userenv->{'branch'}, # branchcode - $onsite_checkout, - $auto_renew ? 1 : 0 # automatic renewal - ); - if ( C4::Context->preference('ReturnToShelvingCart') ) { ## ReturnToShelvingCart is on, anything issued should be taken off the cart. - CartToShelf( $item->{'itemnumber'} ); - } - $item->{'issues'}++; - if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) { - UpdateTotalIssues($item->{'biblionumber'}, 1); - } + # If automatic renewal wasn't selected while issuing, set the value according to the issuing rule. + unless ($auto_renew) { + my $issuingrule = GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branch ); + $auto_renew = $issuingrule->{auto_renew}; + } + + # Record in the database the fact that the book was issued. + unless ($datedue) { + my $itype = + ( C4::Context->preference('item-level_itypes') ) + ? $biblio->{'itype'} + : $biblio->{'itemtype'}; + $datedue = CalcDateDue( $issuedate, $itype, $branch, $borrower ); - ## If item was lost, it has now been found, reverse any list item charges if neccessary. - if ( $item->{'itemlost'} ) { - if ( C4::Context->preference('RefundLostItemFeeOnReturn' ) ) { - _FixAccountForLostAndReturned( $item->{'itemnumber'}, undef, $item->{'barcode'} ); } - } + $datedue->truncate( to => 'minute' ); - ModItem({ issues => $item->{'issues'}, - holdingbranch => C4::Context->userenv->{'branch'}, - itemlost => 0, - datelastborrowed => DateTime->now(time_zone => C4::Context->tz())->ymd(), - onloan => $datedue->ymd(), - }, $item->{'biblionumber'}, $item->{'itemnumber'}); - ModDateLastSeen( $item->{'itemnumber'} ); + $issue = Koha::Database->new()->schema()->resultset('Issue')->update_or_create( + { + borrowernumber => $borrower->{'borrowernumber'}, + itemnumber => $item->{'itemnumber'}, + issuedate => $issuedate->strftime('%Y-%m-%d %H:%M:%S'), + date_due => $datedue->strftime('%Y-%m-%d %H:%M:%S'), + branchcode => C4::Context->userenv->{'branch'}, + onsite_checkout => $onsite_checkout, + auto_renew => $auto_renew ? 1 : 0 + } + ); - # If it costs to borrow this book, charge it to the patron's account. - my ( $charge, $itemtype ) = GetIssuingCharges( - $item->{'itemnumber'}, - $borrower->{'borrowernumber'} - ); - if ( $charge > 0 ) { - AddIssuingCharge( - $item->{'itemnumber'}, - $borrower->{'borrowernumber'}, $charge + if ( C4::Context->preference('ReturnToShelvingCart') ) { + # ReturnToShelvingCart is on, anything issued should be taken off the cart. + CartToShelf( $item->{'itemnumber'} ); + } + $item->{'issues'}++; + if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) { + UpdateTotalIssues( $item->{'biblionumber'}, 1 ); + } + + ## If item was lost, it has now been found, reverse any list item charges if necessary. + if ( $item->{'itemlost'} ) { + if ( + Koha::RefundLostItemFeeRules->should_refund( + { + current_branch => C4::Context->userenv->{branch}, + item_home_branch => $item->{homebranch}, + item_holding_branch => $item->{holdingbranch} + } + ) + ) + { + _FixAccountForLostAndReturned( $item->{'itemnumber'}, undef, + $item->{'barcode'} ); + } + } + + ModItem( + { + issues => $item->{'issues'}, + holdingbranch => C4::Context->userenv->{'branch'}, + itemlost => 0, + onloan => $datedue->ymd(), + datelastborrowed => DateTime->now( time_zone => C4::Context->tz() )->ymd(), + }, + $item->{'biblionumber'}, + $item->{'itemnumber'} ); - $item->{'charge'} = $charge; - } + ModDateLastSeen( $item->{'itemnumber'} ); - # Record the fact that this book was issued. - &UpdateStats({ - branch => C4::Context->userenv->{'branch'}, - type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ), - amount => $charge, - other => ($sipmode ? "SIP-$sipmode" : ''), - itemnumber => $item->{'itemnumber'}, - itemtype => $item->{'itype'}, - borrowernumber => $borrower->{'borrowernumber'}, - ccode => $item->{'ccode'}} - ); + # If it costs to borrow this book, charge it to the patron's account. + my ( $charge, $itemtype ) = GetIssuingCharges( $item->{'itemnumber'}, $borrower->{'borrowernumber'} ); + if ( $charge > 0 ) { + AddIssuingCharge( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $charge ); + $item->{'charge'} = $charge; + } - # Send a checkout slip. - my $circulation_alert = 'C4::ItemCirculationAlertPreference'; - my %conditions = ( - branchcode => $branch, - categorycode => $borrower->{categorycode}, - item_type => $item->{itype}, - notification => 'CHECKOUT', - ); - if ($circulation_alert->is_enabled_for(\%conditions)) { - SendCirculationAlert({ - type => 'CHECKOUT', - item => $item, - borrower => $borrower, - branch => $branch, - }); + # Record the fact that this book was issued. + &UpdateStats( + { + branch => C4::Context->userenv->{'branch'}, + type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ), + amount => $charge, + other => ( $sipmode ? "SIP-$sipmode" : '' ), + itemnumber => $item->{'itemnumber'}, + itemtype => $item->{'itype'}, + borrowernumber => $borrower->{'borrowernumber'}, + ccode => $item->{'ccode'} + } + ); + + # Send a checkout slip. + my $circulation_alert = 'C4::ItemCirculationAlertPreference'; + my %conditions = ( + branchcode => $branch, + categorycode => $borrower->{categorycode}, + item_type => $item->{itype}, + notification => 'CHECKOUT', + ); + if ( $circulation_alert->is_enabled_for( \%conditions ) ) { + SendCirculationAlert( + { + type => 'CHECKOUT', + item => $item, + borrower => $borrower, + branch => $branch, + } + ); + } } - } - logaction("CIRCULATION", "ISSUE", $borrower->{'borrowernumber'}, $biblio->{'itemnumber'}) - if C4::Context->preference("IssueLog"); - } - return ($datedue); # not necessarily the same as when it came in! + logaction( + "CIRCULATION", "ISSUE", + $borrower->{'borrowernumber'}, + $biblio->{'itemnumber'} + ) if C4::Context->preference("IssueLog"); + } + return $issue; } =head2 GetLoanLength @@ -1383,47 +1474,47 @@ sub GetLoanLength { my $loanlength = $sth->fetchrow_hashref; return $loanlength - if defined($loanlength) && $loanlength->{issuelength}; + if defined($loanlength) && defined $loanlength->{issuelength}; $sth->execute( $borrowertype, '*', $branchcode ); $loanlength = $sth->fetchrow_hashref; return $loanlength - if defined($loanlength) && $loanlength->{issuelength}; + if defined($loanlength) && defined $loanlength->{issuelength}; $sth->execute( '*', $itemtype, $branchcode ); $loanlength = $sth->fetchrow_hashref; return $loanlength - if defined($loanlength) && $loanlength->{issuelength}; + if defined($loanlength) && defined $loanlength->{issuelength}; $sth->execute( '*', '*', $branchcode ); $loanlength = $sth->fetchrow_hashref; return $loanlength - if defined($loanlength) && $loanlength->{issuelength}; + if defined($loanlength) && defined $loanlength->{issuelength}; $sth->execute( $borrowertype, $itemtype, '*' ); $loanlength = $sth->fetchrow_hashref; return $loanlength - if defined($loanlength) && $loanlength->{issuelength}; + if defined($loanlength) && defined $loanlength->{issuelength}; $sth->execute( $borrowertype, '*', '*' ); $loanlength = $sth->fetchrow_hashref; return $loanlength - if defined($loanlength) && $loanlength->{issuelength}; + if defined($loanlength) && defined $loanlength->{issuelength}; $sth->execute( '*', $itemtype, '*' ); $loanlength = $sth->fetchrow_hashref; return $loanlength - if defined($loanlength) && $loanlength->{issuelength}; + if defined($loanlength) && defined $loanlength->{issuelength}; $sth->execute( '*', '*', '*' ); $loanlength = $sth->fetchrow_hashref; return $loanlength - if defined($loanlength) && $loanlength->{issuelength}; + if defined($loanlength) && defined $loanlength->{issuelength}; - # if no rule is set => 21 days (hardcoded) + # if no rule is set => 0 day (hardcoded) return { - issuelength => 21, - renewalperiod => 21, + issuelength => 0, + renewalperiod => 0, lengthunit => 'days', }; @@ -1468,10 +1559,10 @@ Returns a hashref from the issuingrules table. sub GetIssuingRule { my ( $borrowertype, $itemtype, $branchcode ) = @_; my $dbh = C4::Context->dbh; - my $sth = $dbh->prepare( "select * from issuingrules where categorycode=? and itemtype=? and branchcode=? and issuelength is not null" ); + my $sth = $dbh->prepare( "select * from issuingrules where categorycode=? and itemtype=? and branchcode=?" ); my $irule; - $sth->execute( $borrowertype, $itemtype, $branchcode ); + $sth->execute( $borrowertype, $itemtype, $branchcode ); $irule = $sth->fetchrow_hashref; return $irule if defined($irule) ; @@ -1519,6 +1610,10 @@ maxissueqty - maximum number of loans that a patron of the given category can have at the given branch. If the value is undef, no limit. +maxonsiteissueqty - maximum of on-site checkouts that a +patron of the given category can have at the given +branch. If the value is undef, no limit. + This will first check for a specific branch and category match from branch_borrower_circ_rules. @@ -1532,6 +1627,7 @@ If no rule has been found in the database, it will default to the buillt in rule: maxissueqty - undef +maxonsiteissueqty - undef C<$branchcode> and C<$categorycode> should contain the literal branch code and patron category code, respectively - no @@ -1540,53 +1636,45 @@ wildcards. =cut sub GetBranchBorrowerCircRule { - my $branchcode = shift; - my $categorycode = shift; + my ( $branchcode, $categorycode ) = @_; - my $branch_cat_query = "SELECT maxissueqty - FROM branch_borrower_circ_rules - WHERE branchcode = ? - AND categorycode = ?"; + my $rules; my $dbh = C4::Context->dbh(); - my $sth = $dbh->prepare($branch_cat_query); - $sth->execute($branchcode, $categorycode); - my $result; - if ($result = $sth->fetchrow_hashref()) { - return $result; - } + $rules = $dbh->selectrow_hashref( q| + SELECT maxissueqty, maxonsiteissueqty + FROM branch_borrower_circ_rules + WHERE branchcode = ? + AND categorycode = ? + |, {}, $branchcode, $categorycode ) ; + return $rules if $rules; # try same branch, default borrower category - my $branch_query = "SELECT maxissueqty - FROM default_branch_circ_rules - WHERE branchcode = ?"; - $sth = $dbh->prepare($branch_query); - $sth->execute($branchcode); - if ($result = $sth->fetchrow_hashref()) { - return $result; - } + $rules = $dbh->selectrow_hashref( q| + SELECT maxissueqty, maxonsiteissueqty + FROM default_branch_circ_rules + WHERE branchcode = ? + |, {}, $branchcode ) ; + return $rules if $rules; # try default branch, same borrower category - my $category_query = "SELECT maxissueqty - FROM default_borrower_circ_rules - WHERE categorycode = ?"; - $sth = $dbh->prepare($category_query); - $sth->execute($categorycode); - if ($result = $sth->fetchrow_hashref()) { - return $result; - } - + $rules = $dbh->selectrow_hashref( q| + SELECT maxissueqty, maxonsiteissueqty + FROM default_borrower_circ_rules + WHERE categorycode = ? + |, {}, $categorycode ) ; + return $rules if $rules; + # try default branch, default borrower category - my $default_query = "SELECT maxissueqty - FROM default_circ_rules"; - $sth = $dbh->prepare($default_query); - $sth->execute(); - if ($result = $sth->fetchrow_hashref()) { - return $result; - } - + $rules = $dbh->selectrow_hashref( q| + SELECT maxissueqty, maxonsiteissueqty + FROM default_circ_rules + |, {} ); + return $rules if $rules; + # built-in default circulation rule return { maxissueqty => undef, + maxonsiteissueqty => undef, }; } @@ -1607,6 +1695,7 @@ holdallowed => Hold policy for this branch and itemtype. Possible values: returnbranch => branch to which to return item. Possible values: noreturn: do not return, let item remain where checked in (floating collections) homebranch: return to item's home branch + holdingbranch: return to issuer branch This searches branchitemrules in the following order: @@ -1625,17 +1714,17 @@ sub GetBranchItemRule { my $result = {}; my @attempts = ( - ['SELECT holdallowed, returnbranch + ['SELECT holdallowed, returnbranch, hold_fulfillment_policy FROM branch_item_rules WHERE branchcode = ? AND itemtype = ?', $branchcode, $itemtype], - ['SELECT holdallowed, returnbranch + ['SELECT holdallowed, returnbranch, hold_fulfillment_policy FROM default_branch_circ_rules WHERE branchcode = ?', $branchcode], - ['SELECT holdallowed, returnbranch + ['SELECT holdallowed, returnbranch, hold_fulfillment_policy FROM default_branch_item_rules WHERE itemtype = ?', $itemtype], - ['SELECT holdallowed, returnbranch + ['SELECT holdallowed, returnbranch, hold_fulfillment_policy FROM default_circ_rules'], ); @@ -1648,11 +1737,13 @@ sub GetBranchItemRule { # defaults tables, we have to check that the key we want is set, not # just that a row was returned $result->{'holdallowed'} = $search_result->{'holdallowed'} unless ( defined $result->{'holdallowed'} ); + $result->{'hold_fulfillment_policy'} = $search_result->{'hold_fulfillment_policy'} unless ( defined $result->{'hold_fulfillment_policy'} ); $result->{'returnbranch'} = $search_result->{'returnbranch'} unless ( defined $result->{'returnbranch'} ); } # built-in default circulation rule $result->{'holdallowed'} = 2 unless ( defined $result->{'holdallowed'} ); + $result->{'hold_fulfillment_policy'} = 'any' unless ( defined $result->{'hold_fulfillment_policy'} ); $result->{'returnbranch'} = 'homebranch' unless ( defined $result->{'returnbranch'} ); return $result; @@ -1725,6 +1816,14 @@ fields from the reserves table of the Koha database, and C. It also has the key C, whose value is either C, C, or 0. +=item C + +Value 1 if return is successful. + +=item C + +If AutomaticItemReturn is disabled, return branch is given as value of NeedsTransfer. + =back C<$iteminformation> is a reference-to-hash, giving information about the @@ -1736,30 +1835,35 @@ patron who last borrowed the book. =cut sub AddReturn { - my ( $barcode, $branch, $exemptfine, $dropbox, $return_date ) = @_; + my ( $barcode, $branch, $exemptfine, $dropbox, $return_date, $dropboxdate ) = @_; - if ($branch and not GetBranchDetail($branch)) { + if ($branch and not Koha::Libraries->find($branch)) { warn "AddReturn error: branch '$branch' not found. Reverting to " . C4::Context->userenv->{'branch'}; undef $branch; } $branch = C4::Context->userenv->{'branch'} unless $branch; # we trust userenv to be a safe fallback/default my $messages; my $borrower; - my $biblio; my $doreturn = 1; my $validTransfert = 0; my $stat_type = 'return'; # get information on item - my $itemnumber = GetItemnumberFromBarcode( $barcode ); - unless ($itemnumber) { - return (0, { BadBarcode => $barcode }); # no barcode means no item or borrower. bail out. + my $item = GetItem( undef, $barcode ); + unless ($item) { + return ( 0, { BadBarcode => $barcode } ); # no barcode means no item or borrower. bail out. } + + my $itemnumber = $item->{ itemnumber }; + + my $item_level_itypes = C4::Context->preference("item-level_itypes"); + my $biblio = $item_level_itypes ? undef : GetBiblioData( $item->{ biblionumber } ); # don't get bib data unless we need it + my $itemtype = $item_level_itypes ? $item->{itype} : $biblio->{itemtype}; + my $issue = GetItemIssue($itemnumber); -# warn Dumper($iteminformation); if ($issue and $issue->{borrowernumber}) { $borrower = C4::Members::GetMemberDetails($issue->{borrowernumber}) - or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existant borrowernumber '$issue->{borrowernumber}'\n" + or die "Data inconsistency: barcode $barcode (itemnumber:$itemnumber) claims to be issued to non-existent borrowernumber '$issue->{borrowernumber}'\n" . Dumper($issue) . "\n"; } else { $messages->{'NotIssued'} = $barcode; @@ -1773,8 +1877,6 @@ sub AddReturn { } } - my $item = GetItem($itemnumber) or die "GetItem($itemnumber) failed"; - if ( $item->{'location'} eq 'PROC' ) { if ( C4::Context->preference("InProcessingToShelvingCart") ) { $item->{'location'} = 'CART'; @@ -1788,9 +1890,9 @@ sub AddReturn { # full item data, but no borrowernumber or checkout info (no issue) # we know GetItem should work because GetItemnumberFromBarcode worked - my $hbr = GetBranchItemRule($item->{'homebranch'}, $item->{'itype'})->{'returnbranch'} || "homebranch"; + my $hbr = GetBranchItemRule($item->{'homebranch'}, $item->{'itype'})->{'returnbranch'} || "homebranch"; # get the proper branch to which to return the item - $hbr = $item->{$hbr} || $branch ; + my $returnbranch = $item->{$hbr} || $branch ; # if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch) my $borrowernumber = $borrower->{'borrowernumber'} || undef; # we don't know if we had a borrower or not @@ -1817,9 +1919,11 @@ sub AddReturn { # check if the book is in a permanent collection.... # FIXME -- This 'PE' attribute is largely undocumented. afaict, there's no user interface that reflects this functionality. - if ( $hbr ) { - my $branches = GetBranches(); # a potentially expensive call for a non-feature. - $branches->{$hbr}->{PE} and $messages->{'IsPermanent'} = $hbr; + if ( $returnbranch ) { + my $library = Koha::Libraries->find($returnbranch); + if ( $library and $library->get_categories->search({'me.categorycode' => 'PE'})->count ) { + $messages->{'IsPermanent'} = $returnbranch; + } } # check if the return is allowed at this branch @@ -1849,50 +1953,28 @@ sub AddReturn { # define circControlBranch only if dropbox mode is set # don't allow dropbox mode to create an invalid entry in issues (issuedate > today) # FIXME: check issuedate > returndate, factoring in holidays - #$circControlBranch = _GetCircControlBranch($item,$borrower) unless ( $item->{'issuedate'} eq C4::Dates->today('iso') );; + $circControlBranch = _GetCircControlBranch($item,$borrower); - $issue->{'overdue'} = DateTime->compare($issue->{'date_due'}, $today ) == -1 ? 1 : 0; + $issue->{'overdue'} = DateTime->compare($issue->{'date_due'}, $dropboxdate ) == -1 ? 1 : 0; } if ($borrowernumber) { if ( ( C4::Context->preference('CalculateFinesOnReturn') && $issue->{'overdue'} ) || $return_date ) { - # we only need to calculate and change the fines if we want to do that on return - # Should be on for hourly loans - my $control = C4::Context->preference('CircControl'); - my $control_branchcode = - ( $control eq 'ItemHomeLibrary' ) ? $item->{homebranch} - : ( $control eq 'PatronLibrary' ) ? $borrower->{branchcode} - : $issue->{branchcode}; - - my $date_returned = - $return_date ? dt_from_string($return_date) : $today; - - my ( $amount, $type, $unitcounttotal ) = - C4::Overdues::CalcFine( $item, $borrower->{categorycode}, - $control_branchcode, $datedue, $date_returned ); - - $type ||= q{}; - - if ( C4::Context->preference('finesMode') eq 'production' ) { - if ( $amount > 0 ) { - C4::Overdues::UpdateFine( $issue->{itemnumber}, - $issue->{borrowernumber}, - $amount, $type, output_pref($datedue) ); - } - elsif ($return_date) { - - # Backdated returns may have fines that shouldn't exist, - # so in this case, we need to drop those fines to 0 - - C4::Overdues::UpdateFine( $issue->{itemnumber}, - $issue->{borrowernumber}, - 0, $type, output_pref($datedue) ); - } - } + _CalculateAndUpdateFine( { issue => $issue, item => $item, borrower => $borrower, return_date => $return_date } ); } - MarkIssueReturned( $borrowernumber, $item->{'itemnumber'}, - $circControlBranch, $return_date, $borrower->{'privacy'} ); + eval { + MarkIssueReturned( $borrowernumber, $item->{'itemnumber'}, + $circControlBranch, $return_date, $borrower->{'privacy'} ); + }; + if ( $@ ) { + $messages->{'Wrongbranch'} = { + Wrongbranch => $branch, + Rightbranch => $message + }; + carp $@; + return ( 0, { WasReturned => 0 }, $issue, $borrower ); + } # FIXME is the "= 1" right? This could be the borrower hash. $messages->{'WasReturned'} = 1; @@ -1904,6 +1986,7 @@ sub AddReturn { # the holdingbranch is updated if the document is returned to another location. # this is always done regardless of whether the item was on loan or not + my $item_holding_branch = $item->{ holdingbranch }; if ($item->{'holdingbranch'} ne $branch) { UpdateHoldingbranch($branch, $item->{'itemnumber'}); $item->{'holdingbranch'} = $branch; # update item data holdingbranch too @@ -1937,9 +2020,20 @@ sub AddReturn { if ( $item->{'itemlost'} ) { $messages->{'WasLost'} = 1; - if ( C4::Context->preference('RefundLostItemFeeOnReturn' ) ) { - _FixAccountForLostAndReturned($item->{'itemnumber'}, $borrowernumber, $barcode); # can tolerate undef $borrowernumber - $messages->{'LostItemFeeRefunded'} = 1; + if ( $item->{'itemlost'} ) { + if ( + Koha::RefundLostItemFeeRules->should_refund( + { + current_branch => C4::Context->userenv->{branch}, + item_home_branch => $item->{homebranch}, + item_holding_branch => $item_holding_branch + } + ) + ) + { + _FixAccountForLostAndReturned( $item->{'itemnumber'}, $borrowernumber, $barcode ); + $messages->{'LostItemFeeRefunded'} = 1; + } } } @@ -1950,6 +2044,7 @@ sub AddReturn { if ( $issue->{overdue} && $issue->{date_due} ) { # fix fine days + $today = $dropboxdate if $dropbox; my ($debardate,$reminder) = _debar_user_on_return( $borrower, $item, $issue->{date_due}, $today ); if ($reminder){ $messages->{'PrevDebarred'} = $debardate; @@ -1982,15 +2077,14 @@ sub AddReturn { } # Record the fact that this book was returned. - # FIXME itemtype should record item level type, not bibliolevel type UpdateStats({ - branch => $branch, - type => $stat_type, - itemnumber => $item->{'itemnumber'}, - itemtype => $biblio->{'itemtype'}, - borrowernumber => $borrowernumber, - ccode => $item->{'ccode'}} - ); + branch => $branch, + type => $stat_type, + itemnumber => $itemnumber, + itemtype => $itemtype, + borrowernumber => $borrowernumber, + ccode => $item->{ ccode } + }); # Send a check-in slip. # NOTE: borrower may be undef. probably shouldn't try to send messages then. my $circulation_alert = 'C4::ItemCirculationAlertPreference'; @@ -2016,27 +2110,24 @@ sub AddReturn { if ( $borrowernumber && $borrower->{'debarred'} && C4::Context->preference('AutoRemoveOverduesRestrictions') - && !HasOverdues( $borrowernumber ) + && !Koha::Patrons->find( $borrowernumber )->has_overdues && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) } ) { DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' }); } - # FIXME: make this comment intelligible. - #adding message if holdingbranch is non equal a userenv branch to return the document to homebranch - #we check, if we don't have reserv or transfert for this document, if not, return it to homebranch . - - if ( !$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $hbr) and not $messages->{'WrongTransfer'}){ - if ( C4::Context->preference("AutomaticItemReturn" ) or + # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer + if (!$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) and not $messages->{'WrongTransfer'}){ + if (C4::Context->preference("AutomaticItemReturn" ) or (C4::Context->preference("UseBranchTransferLimits") and - ! IsBranchTransferAllowed($branch, $hbr, $item->{C4::Context->preference("BranchTransferLimitsType")} ) + ! IsBranchTransferAllowed($branch, $returnbranch, $item->{C4::Context->preference("BranchTransferLimitsType")} ) )) { - $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s)", $item->{'itemnumber'},$branch, $hbr; + $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s)", $item->{'itemnumber'},$branch, $returnbranch; $debug and warn "item: " . Dumper($item); - ModItemTransfer($item->{'itemnumber'}, $branch, $hbr); + ModItemTransfer($item->{'itemnumber'}, $branch, $returnbranch); $messages->{'WasTransfered'} = 1; } else { - $messages->{'NeedsTransfer'} = 1; # TODO: instead of 1, specify branchcode that the transfer SHOULD go to, $item->{homebranch} + $messages->{'NeedsTransfer'} = $returnbranch; } } @@ -2069,6 +2160,16 @@ routine in C. sub MarkIssueReturned { my ( $borrowernumber, $itemnumber, $dropbox_branch, $returndate, $privacy ) = @_; + my $anonymouspatron; + if ( $privacy == 2 ) { + # The default of 0 will not work due to foreign key constraints + # The anonymisation will fail if AnonymousPatron is not a valid entry + # We need to check if the anonymous patron exist, Koha will fail loudly if it does not + # Note that a warning should appear on the about page (System information tab). + $anonymouspatron = C4::Context->preference('AnonymousPatron'); + die "Fatal error: the patron ($borrowernumber) has requested their circulation history be anonymized on check-in, but the AnonymousPatron system preference is empty or not set correctly." + unless C4::Members::GetMember( borrowernumber => $anonymouspatron ); + } my $dbh = C4::Context->dbh; my $query = 'UPDATE issues SET returndate='; my @bind; @@ -2094,10 +2195,6 @@ sub MarkIssueReturned { $sth_copy->execute($borrowernumber, $itemnumber); # anonymise patron checkout immediately if $privacy set to 2 and AnonymousPatron is set to a valid borrowernumber if ( $privacy == 2) { - # The default of 0 does not work due to foreign key constraints - # The anonymisation will fail quietly if AnonymousPatron is not a valid entry - # FIXME the above is unacceptable - bug 9942 relates - my $anonymouspatron = (C4::Context->preference('AnonymousPatron')) ? C4::Context->preference('AnonymousPatron') : 0; my $sth_ano = $dbh->prepare("UPDATE old_issues SET borrowernumber=? WHERE borrowernumber = ? AND itemnumber = ?"); @@ -2109,6 +2206,12 @@ sub MarkIssueReturned { $sth_del->execute($borrowernumber, $itemnumber); ModItem( { 'onloan' => undef }, undef, $itemnumber ); + + if ( C4::Context->preference('StoreLastBorrower') ) { + my $item = Koha::Items->find( $itemnumber ); + my $patron = Koha::Patrons->find( $borrowernumber ); + $item->last_returned_by( $patron ); + } } =head2 _debar_user_on_return @@ -2134,16 +2237,13 @@ sub _debar_user_on_return { my ( $borrower, $item, $dt_due, $dt_today ) = @_; my $branchcode = _GetCircControlBranch( $item, $borrower ); - my $calendar = Koha::Calendar->new( branchcode => $branchcode ); - - # $deltadays is a DateTime::Duration object - my $deltadays = $calendar->days_between( $dt_due, $dt_today ); my $circcontrol = C4::Context->preference('CircControl'); my $issuingrule = GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode ); my $finedays = $issuingrule->{finedays}; my $unit = $issuingrule->{lengthunit}; + my $chargeable_units = C4::Overdues::get_chargeable_units($unit, $dt_due, $dt_today, $branchcode); if ($finedays) { @@ -2155,6 +2255,9 @@ sub _debar_user_on_return { my $grace = DateTime::Duration->new( $unit => $issuingrule->{firstremind} ); + my $deltadays = DateTime::Duration->new( + days => $chargeable_units + ); if ( $deltadays->subtract($grace)->is_positive() ) { my $suspension_days = $deltadays * $finedays; @@ -2170,14 +2273,15 @@ sub _debar_user_on_return { my $new_debar_dt = $dt_today->clone()->add_duration( $suspension_days ); - Koha::Borrower::Debarments::AddUniqueDebarment({ + Koha::Patron::Debarments::AddUniqueDebarment({ borrowernumber => $borrower->{borrowernumber}, expiration => $new_debar_dt->ymd(), type => 'SUSPENSION', }); # if borrower was already debarred but does not get an extra debarment - if ( $borrower->{debarred} eq Koha::Borrower::Debarments::IsDebarred($borrower->{borrowernumber}) ) { - return ($borrower->{debarred},1); + my $patron = Koha::Patrons->find( $borrower->{borrowernumber} ); + if ( $borrower->{debarred} eq $patron->is_debarred ) { + return ($borrower->{debarred},1); } return $new_debar_dt->ymd(); } @@ -2401,6 +2505,8 @@ sub GetItemIssue { $sth->execute($itemnumber); my $data = $sth->fetchrow_hashref; return unless $data; + $data->{issuedate_sql} = $data->{issuedate}; + $data->{date_due_sql} = $data->{date_due}; $data->{issuedate} = dt_from_string($data->{issuedate}, 'sql'); $data->{issuedate}->truncate(to => 'minute'); $data->{date_due} = dt_from_string($data->{date_due}, 'sql'); @@ -2652,6 +2758,7 @@ sub CanBookBeRenewed { my $item = GetItem($itemnumber) or return ( 0, 'no_item' ); my $itemissue = GetItemIssue($itemnumber) or return ( 0, 'no_checkout' ); + return ( 0, 'onsite_checkout' ) if $itemissue->{onsite_checkout}; $borrowernumber ||= $itemissue->{borrowernumber}; my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber ) @@ -2666,54 +2773,63 @@ sub CanBookBeRenewed { { my $schema = Koha::Database->new()->schema(); - # Get all other items that could possibly fill reserves - my @itemnumbers = $schema->resultset('Item')->search( - { - biblionumber => $resrec->{biblionumber}, - onloan => undef, - -not => { itemnumber => $itemnumber } - }, - { columns => 'itemnumber' } - )->get_column('itemnumber')->all(); - - # Get all other reserves that could have been filled by this item - my @borrowernumbers; - while (1) { - my ( $reserve_found, $reserve, undef ) = - C4::Reserves::CheckReserves( $itemnumber, undef, undef, - \@borrowernumbers ); - - if ($reserve_found) { - push( @borrowernumbers, $reserve->{borrowernumber} ); - } - else { - last; - } + my $item_holds = $schema->resultset('Reserve')->search( { itemnumber => $itemnumber, found => undef } )->count(); + if ($item_holds) { + # There is an item level hold on this item, no other item can fill the hold + $resfound = 1; } + else { - # If the count of the union of the lists of reservable items for each borrower - # is equal or greater than the number of borrowers, we know that all reserves - # can be filled with available items. We can get the union of the sets simply - # by pushing all the elements onto an array and removing the duplicates. - my @reservable; - foreach my $b (@borrowernumbers) { - foreach my $i (@itemnumbers) { - if ( IsAvailableForItemLevelRequest($i) - && CanItemBeReserved( $b, $i ) - && !IsItemOnHoldAndFound($i) ) + # Get all other items that could possibly fill reserves + my @itemnumbers = $schema->resultset('Item')->search( { - push( @reservable, $i ); + biblionumber => $resrec->{biblionumber}, + onloan => undef, + notforloan => 0, + -not => { itemnumber => $itemnumber } + }, + { columns => 'itemnumber' } + )->get_column('itemnumber')->all(); + + # Get all other reserves that could have been filled by this item + my @borrowernumbers; + while (1) { + my ( $reserve_found, $reserve, undef ) = + C4::Reserves::CheckReserves( $itemnumber, undef, undef, \@borrowernumbers ); + + if ($reserve_found) { + push( @borrowernumbers, $reserve->{borrowernumber} ); + } + else { + last; + } + } + + # If the count of the union of the lists of reservable items for each borrower + # is equal or greater than the number of borrowers, we know that all reserves + # can be filled with available items. We can get the union of the sets simply + # by pushing all the elements onto an array and removing the duplicates. + my @reservable; + foreach my $b (@borrowernumbers) { + my ($borr) = C4::Members::GetMemberDetails($b); + foreach my $i (@itemnumbers) { + my $item = GetItem($i); + if ( IsAvailableForItemLevelRequest( $item, $borr ) + && CanItemBeReserved( $b, $i ) + && !IsItemOnHoldAndFound($i) ) + { + push( @reservable, $i ); + } } } - } - @reservable = uniq(@reservable); + @reservable = uniq(@reservable); - if ( @reservable >= @borrowernumbers ) { - $resfound = 0; + if ( @reservable >= @borrowernumbers ) { + $resfound = 0; + } } } - return ( 0, "on_reserve" ) if $resfound; # '' when no hold was found return ( 1, undef ) if $override_limit; @@ -2725,22 +2841,54 @@ sub CanBookBeRenewed { return ( 0, "too_many" ) if $issuingrule->{renewalsallowed} <= $itemissue->{renewals}; - if ( $issuingrule->{norenewalbefore} ) { + my $overduesblockrenewing = C4::Context->preference('OverduesBlockRenewing'); + my $restrictionblockrenewing = C4::Context->preference('RestrictionBlockRenewing'); + my $patron = Koha::Patrons->find($borrowernumber); + my $restricted = $patron->is_debarred; + my $hasoverdues = $patron->has_overdues; + + if ( $restricted and $restrictionblockrenewing ) { + return ( 0, 'restriction'); + } elsif ( ($hasoverdues and $overduesblockrenewing eq 'block') || ($itemissue->{overdue} and $overduesblockrenewing eq 'blockitem') ) { + return ( 0, 'overdue'); + } + + if ( defined $issuingrule->{norenewalbefore} + and $issuingrule->{norenewalbefore} ne "" ) + { + + # Calculate soonest renewal by subtracting 'No renewal before' from due date + my $soonestrenewal = + $itemissue->{date_due}->clone() + ->subtract( + $issuingrule->{lengthunit} => $issuingrule->{norenewalbefore} ); + + # Depending on syspref reset the exact time, only check the date + if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date' + and $issuingrule->{lengthunit} eq 'days' ) + { + $soonestrenewal->truncate( to => 'day' ); + } - # Get current time and add norenewalbefore. - # If this is smaller than date_due, it's too soon for renewal. - if ( - DateTime->now( time_zone => C4::Context->tz() )->add( - $issuingrule->{lengthunit} => $issuingrule->{norenewalbefore} - ) < $itemissue->{date_due} - ) + if ( $soonestrenewal > DateTime->now( time_zone => C4::Context->tz() ) ) { return ( 0, "auto_too_soon" ) if $itemissue->{auto_renew}; return ( 0, "too_soon" ); } + elsif ( $itemissue->{auto_renew} ) { + return ( 0, "auto_renew" ); + } + } + + # Fallback for automatic renewals: + # If norenewalbefore is undef, don't renew before due date. + elsif ( $itemissue->{auto_renew} ) { + my $now = dt_from_string; + return ( 0, "auto_renew" ) + if $now >= $itemissue->{date_due}; + return ( 0, "auto_too_soon" ); } - return ( 0, "auto_renew" ) if $itemissue->{auto_renew}; return ( 1, undef ); } @@ -2758,7 +2906,7 @@ C<$itemnumber> is the number of the item to renew. C<$branch> is the library where the renewal took place (if any). The library that controls the circ policies for the renewal is retrieved from the issues record. -C<$datedue> can be a C4::Dates object used to set the due date. +C<$datedue> can be a DateTime object used to set the due date. C<$lastreneweddate> is an optional ISO-formatted date used to set issues.lastreneweddate. If this parameter is not supplied, lastreneweddate is set to the current date. @@ -2781,10 +2929,7 @@ sub AddRenewal { my $dbh = C4::Context->dbh; # Find the issues record for this book - my $sth = - $dbh->prepare("SELECT * FROM issues WHERE itemnumber = ?"); - $sth->execute( $itemnumber ); - my $issuedata = $sth->fetchrow_hashref; + my $issuedata = GetItemIssue($itemnumber); return unless ( $issuedata ); @@ -2795,12 +2940,18 @@ sub AddRenewal { return; } + my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber ) or return; + + if ( C4::Context->preference('CalculateFinesOnReturn') && $issuedata->{overdue} ) { + _CalculateAndUpdateFine( { issue => $issuedata, item => $item, borrower => $borrower } ); + } + _FixOverduesOnReturn( $borrowernumber, $itemnumber ); + # If the due date wasn't specified, calculate it by adding the # book's loan length to today's date or the current due date # based on the value of the RenewalPeriodBase syspref. unless ($datedue) { - my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber ) or return; my $itemtype = (C4::Context->preference('item-level_itypes')) ? $biblio->{'itype'} : $biblio->{'itemtype'}; $datedue = (C4::Context->preference('RenewalPeriodBase') eq 'date_due') ? @@ -2812,7 +2963,7 @@ sub AddRenewal { # Update the issues record to have the new due date, and a new count # of how many times it has been renewed. my $renews = $issuedata->{'renewals'} + 1; - $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ? + my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, lastreneweddate = ? WHERE borrowernumber=? AND itemnumber=?" ); @@ -2842,30 +2993,32 @@ sub AddRenewal { } # Send a renewal slip according to checkout alert preferencei - if ( C4::Context->preference('RenewalSendNotice') eq '1') { - my $borrower = C4::Members::GetMemberDetails( $borrowernumber, 0 ); - my $circulation_alert = 'C4::ItemCirculationAlertPreference'; - my %conditions = ( - branchcode => $branch, - categorycode => $borrower->{categorycode}, - item_type => $item->{itype}, - notification => 'CHECKOUT', - ); - if ($circulation_alert->is_enabled_for(\%conditions)) { - SendCirculationAlert({ - type => 'RENEWAL', - item => $item, - borrower => $borrower, - branch => $branch, - }); - } + if ( C4::Context->preference('RenewalSendNotice') eq '1' ) { + $borrower = C4::Members::GetMemberDetails( $borrowernumber, 0 ); + my $circulation_alert = 'C4::ItemCirculationAlertPreference'; + my %conditions = ( + branchcode => $branch, + categorycode => $borrower->{categorycode}, + item_type => $item->{itype}, + notification => 'CHECKOUT', + ); + if ( $circulation_alert->is_enabled_for( \%conditions ) ) { + SendCirculationAlert( + { + type => 'RENEWAL', + item => $item, + borrower => $borrower, + branch => $branch, + } + ); + } } # Remove any OVERDUES related debarment if the borrower has no overdues - my $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber ); + $borrower = C4::Members::GetMember( borrowernumber => $borrowernumber ); if ( $borrowernumber && $borrower->{'debarred'} - && !HasOverdues( $borrowernumber ) + && !Koha::Patrons->find( $borrowernumber )->has_overdues && @{ GetDebarments({ borrowernumber => $borrowernumber, type => 'OVERDUES' }) } ) { DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' }); @@ -2952,15 +3105,22 @@ sub GetSoonestRenewDate { my $issuingrule = GetIssuingRule( $borrower->{categorycode}, $item->{itype}, $branchcode ); - my $now = DateTime->now( time_zone => C4::Context->tz() ); + my $now = dt_from_string; - if ( $issuingrule->{norenewalbefore} ) { + if ( defined $issuingrule->{norenewalbefore} + and $issuingrule->{norenewalbefore} ne "" ) + { my $soonestrenewal = - $itemissue->{date_due}->subtract( + $itemissue->{date_due}->clone() + ->subtract( $issuingrule->{lengthunit} => $issuingrule->{norenewalbefore} ); - $soonestrenewal = $now > $soonestrenewal ? $now : $soonestrenewal; - return $soonestrenewal; + if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date' + and $issuingrule->{lengthunit} eq 'days' ) + { + $soonestrenewal->truncate( to => 'day' ); + } + return $soonestrenewal if $now < $soonestrenewal; } return $now; } @@ -3004,7 +3164,7 @@ sub GetIssuingCharges { if ( my $item_data = $sth->fetchrow_hashref ) { $item_type = $item_data->{itemtype}; $charge = $item_data->{rentalcharge}; - my $branch = C4::Branch::mybranch(); + my $branch = C4::Context::mybranch(); my $discount_query = q|SELECT rentaldiscount, issuingrules.itemtype, issuingrules.branchcode FROM borrowers @@ -3183,8 +3343,9 @@ sub AnonymiseIssueHistory { "; # The default of 0 does not work due to foreign key constraints - # The anonymisation will fail quietly if AnonymousPatron is not a valid entry - my $anonymouspatron = (C4::Context->preference('AnonymousPatron')) ? C4::Context->preference('AnonymousPatron') : 0; + # The anonymisation should not fail quietly if AnonymousPatron is not a valid entry + # Set it to undef (NULL) + my $anonymouspatron = C4::Context->preference('AnonymousPatron') || undef; my @bind_params = ($anonymouspatron, $date); if (defined $borrowernumber) { $query .= " AND borrowernumber = ?"; @@ -3250,19 +3411,6 @@ sub SendCirculationAlert { message_name => $message_name{$type}, }); my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues'; - my $letter = C4::Letters::GetPreparedLetter ( - module => 'circulation', - letter_code => $type, - branchcode => $branch, - tables => { - $issues_table => $item->{itemnumber}, - 'items' => $item->{itemnumber}, - 'biblio' => $item->{biblionumber}, - 'biblioitems' => $item->{biblionumber}, - 'borrowers' => $borrower, - 'branches' => $branch, - } - ) or return; my @transports = keys %{ $borrower_preferences->{transports} }; # warn "no transports" unless @transports; @@ -3271,15 +3419,43 @@ sub SendCirculationAlert { my $message = C4::Message->find_last_message($borrower, $type, $_); if (!$message) { #warn "create new message"; + my $letter = C4::Letters::GetPreparedLetter ( + module => 'circulation', + letter_code => $type, + branchcode => $branch, + message_transport_type => $_, + tables => { + $issues_table => $item->{itemnumber}, + 'items' => $item->{itemnumber}, + 'biblio' => $item->{biblionumber}, + 'biblioitems' => $item->{biblionumber}, + 'borrowers' => $borrower, + 'branches' => $branch, + } + ) or next; C4::Message->enqueue($letter, $borrower, $_); } else { #warn "append to old message"; + my $letter = C4::Letters::GetPreparedLetter ( + module => 'circulation', + letter_code => $type, + branchcode => $branch, + message_transport_type => $_, + tables => { + $issues_table => $item->{itemnumber}, + 'items' => $item->{itemnumber}, + 'biblio' => $item->{biblionumber}, + 'biblioitems' => $item->{biblionumber}, + 'borrowers' => $borrower, + 'branches' => $branch, + } + ) or next; $message->append($letter); $message->update; } } - return $letter; + return; } =head2 updateWrongTransfer @@ -3326,7 +3502,7 @@ $newdatedue = CalcDateDue($startdate,$itemtype,$branchcode,$borrower); this function calculates the due date given the start date and configured circulation rules, checking against the holidays calendar as per the 'useDaysMode' syspref. -C<$startdate> = C4::Dates object representing start date of loan period (assumed to be today) +C<$startdate> = DateTime object representing start date of loan period (assumed to be today) C<$itemtype> = itemtype code of item in question C<$branch> = location whose calendar to use C<$borrower> = Borrower object @@ -3387,7 +3563,7 @@ sub CalcDateDue { } } - # if Hard Due Dates are used, retreive them and apply as necessary + # if Hard Due Dates are used, retrieve them and apply as necessary my ( $hardduedate, $hardduedatecompare ) = GetHardDueDate( $borrower->{'categorycode'}, $itemtype, $branch ); if ($hardduedate) { # hardduedates are currently dates @@ -3409,10 +3585,13 @@ sub CalcDateDue { # if ReturnBeforeExpiry ON the datedue can't be after borrower expirydate if ( C4::Context->preference('ReturnBeforeExpiry') ) { - my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso' ); - $expiry_dt->set( hour => 23, minute => 59); - if ( DateTime->compare( $datedue, $expiry_dt ) == 1 ) { - $datedue = $expiry_dt->clone; + my $expiry_dt = dt_from_string( $borrower->{dateexpiry}, 'iso', 'floating'); + if( $expiry_dt ) { #skip empty expiry date.. + $expiry_dt->set( hour => 23, minute => 59); + my $d1= $datedue->clone->set_time_zone('floating'); + if ( DateTime->compare( $d1, $expiry_dt ) == 1 ) { + $datedue = $expiry_dt->clone->set_time_zone( C4::Context->tz ); + } } } @@ -3420,92 +3599,6 @@ sub CalcDateDue { } -=head2 CheckRepeatableHolidays - - $countrepeatable = CheckRepeatableHoliday($itemnumber,$week_day,$branchcode); - -This function checks if the date due is a repeatable holiday - -C<$date_due> = returndate calculate with no day check -C<$itemnumber> = itemnumber -C<$branchcode> = localisation of issue - -=cut - -sub CheckRepeatableHolidays{ -my($itemnumber,$week_day,$branchcode)=@_; -my $dbh = C4::Context->dbh; -my $query = qq|SELECT count(*) - FROM repeatable_holidays - WHERE branchcode=? - AND weekday=?|; -my $sth = $dbh->prepare($query); -$sth->execute($branchcode,$week_day); -my $result=$sth->fetchrow; -return $result; -} - - -=head2 CheckSpecialHolidays - - $countspecial = CheckSpecialHolidays($years,$month,$day,$itemnumber,$branchcode); - -This function check if the date is a special holiday - -C<$years> = the years of datedue -C<$month> = the month of datedue -C<$day> = the day of datedue -C<$itemnumber> = itemnumber -C<$branchcode> = localisation of issue - -=cut - -sub CheckSpecialHolidays{ -my ($years,$month,$day,$itemnumber,$branchcode) = @_; -my $dbh = C4::Context->dbh; -my $query=qq|SELECT count(*) - FROM `special_holidays` - WHERE year=? - AND month=? - AND day=? - AND branchcode=? - |; -my $sth = $dbh->prepare($query); -$sth->execute($years,$month,$day,$branchcode); -my $countspecial=$sth->fetchrow ; -return $countspecial; -} - -=head2 CheckRepeatableSpecialHolidays - - $countspecial = CheckRepeatableSpecialHolidays($month,$day,$itemnumber,$branchcode); - -This function check if the date is a repeatble special holidays - -C<$month> = the month of datedue -C<$day> = the day of datedue -C<$itemnumber> = itemnumber -C<$branchcode> = localisation of issue - -=cut - -sub CheckRepeatableSpecialHolidays{ -my ($month,$day,$itemnumber,$branchcode) = @_; -my $dbh = C4::Context->dbh; -my $query=qq|SELECT count(*) - FROM `repeatable_holidays` - WHERE month=? - AND day=? - AND branchcode=? - |; -my $sth = $dbh->prepare($query); -$sth->execute($month,$day,$branchcode); -my $countspecial=$sth->fetchrow ; -return $countspecial; -} - - - sub CheckValidBarcode{ my ($barcode) = @_; my $dbh = C4::Context->dbh; @@ -3742,10 +3835,10 @@ sub ProcessOfflineIssue { sub ProcessOfflinePayment { my $operation = shift; - my $borrower = C4::Members::GetMemberDetails( undef, $operation->{cardnumber} ); # Get borrower from operation cardnumber + my $patron = Koha::Patrons->find( { cardnumber => $operation->{cardnumber} }); my $amount = $operation->{amount}; - recordpayment( $borrower->{borrowernumber}, $amount ); + Koha::Account->new( { patron_id => $patron->id } )->pay( { amount => $amount } ); return "Success." } @@ -3753,20 +3846,18 @@ sub ProcessOfflinePayment { =head2 TransferSlip - TransferSlip($user_branch, $itemnumber, $to_branch) + TransferSlip($user_branch, $itemnumber, $barcode, $to_branch) Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef =cut sub TransferSlip { - my ($branch, $itemnumber, $to_branch) = @_; + my ($branch, $itemnumber, $barcode, $to_branch) = @_; - my $item = GetItem( $itemnumber ) + my $item = GetItem( $itemnumber, $barcode ) or return; - my $pulldate = C4::Dates->new(); - return C4::Letters::GetPreparedLetter ( module => 'circulation', letter_code => 'TRANSFERSLIP', @@ -3790,12 +3881,15 @@ sub TransferSlip { sub CheckIfIssuedToPatron { my ($borrowernumber, $biblionumber) = @_; - my $items = GetItemsByBiblioitemnumber($biblionumber); - - foreach my $item (@{$items}) { - return 1 if ($item->{borrowernumber} && $item->{borrowernumber} eq $borrowernumber); - } - + my $dbh = C4::Context->dbh; + my $query = q| + SELECT COUNT(*) FROM issues + LEFT JOIN items ON items.itemnumber = issues.itemnumber + WHERE items.biblionumber = ? + AND issues.borrowernumber = ? + |; + my $is_issued = $dbh->selectrow_array($query, {}, $biblionumber, $borrowernumber ); + return 1 if $is_issued; return; } @@ -3842,7 +3936,7 @@ sub GetAgeRestriction { my @values = split ' ', uc($record_restrictions); return unless @values; - # Search first occurence of one of the markers + # Search first occurrence of one of the markers my @markers = split /\|/, uc($markers); return unless @markers; @@ -3881,7 +3975,8 @@ sub GetAgeRestriction { } #Get how many days the borrower has to reach the age restriction - my $daysToAgeRestriction = Date_to_Days(@alloweddate) - Date_to_Days(Today); + my @Today = split /-/, DateTime->today->ymd(); + my $daysToAgeRestriction = Date_to_Days(@alloweddate) - Date_to_Days(@Today); #Negative days means the borrower went past the age restriction age return ($restriction_year, $daysToAgeRestriction); } @@ -3890,6 +3985,155 @@ sub GetAgeRestriction { return ($restriction_year); } + +=head2 GetPendingOnSiteCheckouts + +=cut + +sub GetPendingOnSiteCheckouts { + my $dbh = C4::Context->dbh; + return $dbh->selectall_arrayref(q| + SELECT + items.barcode, + items.biblionumber, + items.itemnumber, + items.itemnotes, + items.itemcallnumber, + items.location, + issues.date_due, + issues.branchcode, + issues.date_due < NOW() AS is_overdue, + biblio.author, + biblio.title, + borrowers.firstname, + borrowers.surname, + borrowers.cardnumber, + borrowers.borrowernumber + FROM items + LEFT JOIN issues ON items.itemnumber = issues.itemnumber + LEFT JOIN biblio ON items.biblionumber = biblio.biblionumber + LEFT JOIN borrowers ON issues.borrowernumber = borrowers.borrowernumber + WHERE issues.onsite_checkout = 1 + |, { Slice => {} } ); +} + +sub GetTopIssues { + my ($params) = @_; + + my ($count, $branch, $itemtype, $ccode, $newness) + = @$params{qw(count branch itemtype ccode newness)}; + + my $dbh = C4::Context->dbh; + my $query = q{ + SELECT b.biblionumber, b.title, b.author, bi.itemtype, bi.publishercode, + bi.place, bi.publicationyear, b.copyrightdate, bi.pages, bi.size, + i.ccode, SUM(i.issues) AS count + FROM biblio b + LEFT JOIN items i ON (i.biblionumber = b.biblionumber) + LEFT JOIN biblioitems bi ON (bi.biblionumber = b.biblionumber) + }; + + my (@where_strs, @where_args); + + if ($branch) { + push @where_strs, 'i.homebranch = ?'; + push @where_args, $branch; + } + if ($itemtype) { + if (C4::Context->preference('item-level_itypes')){ + push @where_strs, 'i.itype = ?'; + push @where_args, $itemtype; + } else { + push @where_strs, 'bi.itemtype = ?'; + push @where_args, $itemtype; + } + } + if ($ccode) { + push @where_strs, 'i.ccode = ?'; + push @where_args, $ccode; + } + if ($newness) { + push @where_strs, 'TO_DAYS(NOW()) - TO_DAYS(b.datecreated) <= ?'; + push @where_args, $newness; + } + + if (@where_strs) { + $query .= 'WHERE ' . join(' AND ', @where_strs); + } + + $query .= q{ + GROUP BY b.biblionumber + HAVING count > 0 + ORDER BY count DESC + }; + + $count = int($count); + if ($count > 0) { + $query .= "LIMIT $count"; + } + + my $rows = $dbh->selectall_arrayref($query, { Slice => {} }, @where_args); + + return @$rows; +} + +sub _CalculateAndUpdateFine { + my ($params) = @_; + + my $borrower = $params->{borrower}; + my $item = $params->{item}; + my $issue = $params->{issue}; + my $return_date = $params->{return_date}; + + unless ($borrower) { carp "No borrower passed in!" && return; } + unless ($item) { carp "No item passed in!" && return; } + unless ($issue) { carp "No issue passed in!" && return; } + + my $datedue = $issue->{date_due}; + + # we only need to calculate and change the fines if we want to do that on return + # Should be on for hourly loans + my $control = C4::Context->preference('CircControl'); + my $control_branchcode = + ( $control eq 'ItemHomeLibrary' ) ? $item->{homebranch} + : ( $control eq 'PatronLibrary' ) ? $borrower->{branchcode} + : $issue->{branchcode}; + + my $date_returned = $return_date ? dt_from_string($return_date) : dt_from_string(); + + my ( $amount, $type, $unitcounttotal ) = + C4::Overdues::CalcFine( $item, $borrower->{categorycode}, $control_branchcode, $datedue, $date_returned ); + + $type ||= q{}; + + if ( C4::Context->preference('finesMode') eq 'production' ) { + if ( $amount > 0 ) { + C4::Overdues::UpdateFine({ + issue_id => $issue->{issue_id}, + itemnumber => $issue->{itemnumber}, + borrowernumber => $issue->{borrowernumber}, + amount => $amount, + type => $type, + due => output_pref($datedue), + }); + } + elsif ($return_date) { + + # Backdated returns may have fines that shouldn't exist, + # so in this case, we need to drop those fines to 0 + + C4::Overdues::UpdateFine({ + issue_id => $issue->{issue_id}, + itemnumber => $issue->{itemnumber}, + borrowernumber => $issue->{borrowernumber}, + amount => 0, + type => $type, + due => output_pref($datedue), + }); + } + } +} + 1; __END__ @@ -3899,4 +4143,3 @@ __END__ Koha Development Team =cut -