X-Git-Url: http://koha-dev.rot13.org:8081/gitweb/?a=blobdiff_plain;f=C4%2FCirculation.pm;h=17cab0c74bc0f9e4877b6ec52fafc93e3d533664;hb=5aee0f6a2a06fedcfdabd34c1757a7a1455a6fcd;hp=50c436ecba21ee5ae64da43817551de9a72c7a5e;hpb=489b08ab4c1da6933b6d23483aab75a6c2e923b5;p=koha-ffzg.git diff --git a/C4/Circulation.pm b/C4/Circulation.pm index 50c436ecba..17cab0c74b 100644 --- a/C4/Circulation.pm +++ b/C4/Circulation.pm @@ -21,33 +21,34 @@ package C4::Circulation; use Modern::Perl; use DateTime; use POSIX qw( floor ); -use Koha::DateUtils; +use YAML::XS; +use Encode; + +use Koha::DateUtils qw( dt_from_string output_pref ); use C4::Context; -use C4::Stats; -use C4::Reserves; -use C4::Biblio; -use C4::Items; -use C4::Members; +use C4::Stats qw( UpdateStats ); +use C4::Reserves qw( CheckReserves CanItemBeReserved MoveReserve ModReserve ModReserveMinusPriority RevertWaitingStatus IsItemOnHoldAndFound IsAvailableForItemLevelRequest ); +use C4::Biblio qw( UpdateTotalIssues ); +use C4::Items qw( ModItemTransfer ModDateLastSeen CartToShelf ); use C4::Accounts; use C4::ItemCirculationAlertPreference; use C4::Message; -use C4::Debug; -use C4::Log; # logaction -use C4::Overdues qw(CalcFine UpdateFine get_chargeable_units); +use C4::Log qw( logaction ); # logaction +use C4::Overdues; use C4::RotatingCollections qw(GetCollectionItemBranches); -use Algorithm::CheckDigits; +use Algorithm::CheckDigits qw( CheckDigits ); -use Data::Dumper; +use Data::Dumper qw( Dumper ); use Koha::Account; use Koha::AuthorisedValues; use Koha::Biblioitems; -use Koha::DateUtils; +use Koha::DateUtils qw( dt_from_string output_pref ); use Koha::Calendar; use Koha::Checkouts; use Koha::Illrequests; use Koha::Items; use Koha::Patrons; -use Koha::Patron::Debarments; +use Koha::Patron::Debarments qw( DelUniqueDebarment GetDebarments AddUniqueDebarment ); use Koha::Database; use Koha::Libraries; use Koha::Account::Lines; @@ -56,81 +57,73 @@ use Koha::Account::Lines; use Koha::Account::Offsets; use Koha::Config::SysPrefs; use Koha::Charges::Fees; -use Koha::Util::SystemPreferences; +use Koha::Config::SysPref; use Koha::Checkouts::ReturnClaims; use Koha::SearchEngine::Indexer; -use Carp; -use List::MoreUtils qw( uniq any ); +use Koha::Exceptions::Checkout; +use Koha::Plugins; +use Carp qw( carp ); +use List::MoreUtils qw( any ); use Scalar::Util qw( looks_like_number ); -use Try::Tiny; -use Date::Calc qw( - Today - Today_and_Now - Add_Delta_YM - Add_Delta_DHMS - Date_to_Days - Day_of_Week - Add_Delta_Days -); -use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); - +use Date::Calc qw( Date_to_Days ); +our (@ISA, @EXPORT_OK); BEGIN { - require Exporter; - @ISA = qw(Exporter); - - # FIXME subs that should probably be elsewhere - push @EXPORT, qw( - &barcodedecode - &LostItem - &ReturnLostItem - &GetPendingOnSiteCheckouts - ); - - # subs to deal with issuing a book - push @EXPORT, qw( - &CanBookBeIssued - &CanBookBeRenewed - &AddIssue - &AddRenewal - &GetRenewCount - &GetSoonestRenewDate - &GetLatestAutoRenewDate - &GetIssuingCharges - &GetBranchBorrowerCircRule - &GetBranchItemRule - &GetOpenIssue - &CheckIfIssuedToPatron - &IsItemIssued - GetTopIssues - ); - - # subs to deal with returns - push @EXPORT, qw( - &AddReturn - &MarkIssueReturned - ); - - # subs to deal with transfers - push @EXPORT, qw( - &transferbook - &GetTransfers - &GetTransfersFromTo - &updateWrongTransfer - &DeleteTransfer - &IsBranchTransferAllowed - &CreateBranchTransferLimit - &DeleteBranchTransferLimits - &TransferSlip - ); - - # subs to deal with offline circulation - push @EXPORT, qw( - &GetOfflineOperations - &GetOfflineOperation - &AddOfflineOperation - &DeleteOfflineOperation - &ProcessOfflineOperation + + require Exporter; + @ISA = qw(Exporter); + + # FIXME subs that should probably be elsewhere + push @EXPORT_OK, qw( + barcodedecode + LostItem + ReturnLostItem + GetPendingOnSiteCheckouts + + CanBookBeIssued + checkHighHolds + CanBookBeRenewed + AddIssue + GetLoanLength + GetHardDueDate + AddRenewal + GetRenewCount + GetSoonestRenewDate + GetLatestAutoRenewDate + GetIssuingCharges + AddIssuingCharge + GetBranchBorrowerCircRule + GetBranchItemRule + GetBiblioIssues + GetOpenIssue + GetUpcomingDueIssues + CheckIfIssuedToPatron + IsItemIssued + GetAgeRestriction + GetTopIssues + + AddReturn + MarkIssueReturned + + transferbook + TooMany + GetTransfers + GetTransfersFromTo + updateWrongTransfer + CalcDateDue + CheckValidBarcode + IsBranchTransferAllowed + CreateBranchTransferLimit + DeleteBranchTransferLimits + TransferSlip + + GetOfflineOperations + GetOfflineOperation + AddOfflineOperation + DeleteOfflineOperation + ProcessOfflineOperation + ProcessOfflinePayment ); + push @EXPORT_OK, '_GetCircControlBranch'; # This is wrong! } =head1 NAME @@ -173,13 +166,14 @@ sub barcodedecode { my ($barcode, $filter) = @_; my $branch = C4::Context::mybranch(); $filter = C4::Context->preference('itemBarcodeInputFilter') unless $filter; + Koha::Plugins->call('item_barcode_transform', \$barcode ); $filter or return $barcode; # ensure filter is defined, else return untouched barcode if ($filter eq 'whitespace') { $barcode =~ s/\s//g; } elsif ($filter eq 'cuecat') { chomp($barcode); my @fields = split( /\./, $barcode ); - my @results = map( decode($_), @fields[ 1 .. $#fields ] ); + my @results = map( C4::Circulation::_decode($_), @fields[ 1 .. $#fields ] ); ($#results == 2) and return $results[2]; } elsif ($filter eq 'T-prefix') { if ($barcode =~ /^[Tt](\d)/) { @@ -210,9 +204,9 @@ sub barcodedecode { return $barcode; # return barcode, modified or not } -=head2 decode +=head2 _decode - $str = &decode($chunk); + $str = &_decode($chunk); Decodes a segment of a string emitted by a CueCat barcode scanner and returns it. @@ -222,7 +216,7 @@ or Javascript based decoding on the client side. =cut -sub decode { +sub _decode { my ($encoded) = @_; my $seq = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-'; @@ -372,10 +366,10 @@ sub transferbook { # That'll save a database query. my ( $resfound, $resrec, undef ) = CheckReserves( $itemnumber ); - if ( $resfound and not $ignoreRs ) { + if ( $resfound ) { $resrec->{'ResFound'} = $resfound; $messages->{'ResFound'} = $resrec; - $dotransfer = 1; + $dotransfer = 0 unless $ignoreRs; } #actually do the transfer.... @@ -383,7 +377,7 @@ sub transferbook { ModItemTransfer( $itemnumber, $fbr, $tbr, $trigger ); # don't need to update MARC anymore, we do it in batch now - $messages->{'WasTransfered'} = 1; + $messages->{'WasTransfered'} = $tbr; } ModDateLastSeen( $itemnumber ); @@ -716,6 +710,10 @@ issued to someone else. reserved for someone else. +=head3 TRANSFERRED + +reserved and being transferred for someone else. + =head3 INVALID_DATE sticky due date is invalid or due date in the past @@ -794,7 +792,7 @@ sub CanBookBeIssued { # if ( $patron->category->category_type eq 'X' && ( $item_object->barcode )) { # stats only borrower -- add entry to statistics table, and return issuingimpossible{STATS} = 1 . - &UpdateStats({ + C4::Stats::UpdateStats({ branch => C4::Context->userenv->{'branch'}, type => 'localuse', itemnumber => $item_object->itemnumber, @@ -839,8 +837,8 @@ sub CanBookBeIssued { 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 @guarantees = map { $_->guarantee } $patron->guarantee_relationships(); - my $guarantees_non_issues_charges; + my @guarantees = map { $_->guarantee } $patron->guarantee_relationships->as_list; + my $guarantees_non_issues_charges = 0; foreach my $g ( @guarantees ) { $guarantees_non_issues_charges += $g->account->non_issues_charges; } @@ -1128,6 +1126,28 @@ sub CanBookBeIssued { $needsconfirmation{'resreservedate'} = $res->{reservedate}; $needsconfirmation{'reserve_id'} = $res->{reserve_id}; } + elsif ( $restype eq "Transferred" ) { + # The item is determined hold being transferred for someone else. + $needsconfirmation{TRANSFERRED} = 1; + $needsconfirmation{'resfirstname'} = $patron->firstname; + $needsconfirmation{'ressurname'} = $patron->surname; + $needsconfirmation{'rescardnumber'} = $patron->cardnumber; + $needsconfirmation{'resborrowernumber'} = $patron->borrowernumber; + $needsconfirmation{'resbranchcode'} = $patron->branchcode; + $needsconfirmation{'resreservedate'} = $res->{reservedate}; + $needsconfirmation{'reserve_id'} = $res->{reserve_id}; + } + elsif ( $restype eq "Processing" ) { + # The item is determined hold being processed for someone else. + $needsconfirmation{PROCESSING} = 1; + $needsconfirmation{'resfirstname'} = $patron->firstname; + $needsconfirmation{'ressurname'} = $patron->surname; + $needsconfirmation{'rescardnumber'} = $patron->cardnumber; + $needsconfirmation{'resborrowernumber'} = $patron->borrowernumber; + $needsconfirmation{'resbranchcode'} = $patron->branchcode; + $needsconfirmation{'resreservedate'} = $res->{reservedate}; + $needsconfirmation{'reserve_id'} = $res->{reserve_id}; + } } } } @@ -1146,7 +1166,7 @@ sub CanBookBeIssued { ## check for high holds decreasing loan period if ( C4::Context->preference('decreaseLoanHighHolds') ) { - my $check = checkHighHolds( $item_unblessed, $patron_unblessed ); + my $check = checkHighHolds( $item_object, $patron ); if ( $check->{exceeded} ) { if ($override_high_holds) { @@ -1258,9 +1278,8 @@ sub CanBookBeReturned { =cut sub checkHighHolds { - my ( $item, $borrower ) = @_; - my $branchcode = _GetCircControlBranch( $item, $borrower ); - my $item_object = Koha::Items->find( $item->{itemnumber} ); + my ( $item, $patron ) = @_; + my $branchcode = _GetCircControlBranch( $item->unblessed, $patron->unblessed ); my $return_data = { exceeded => 0, @@ -1269,7 +1288,7 @@ sub checkHighHolds { due_date => undef, }; - my $holds = Koha::Holds->search( { biblionumber => $item->{'biblionumber'} } ); + my $holds = Koha::Holds->search( { biblionumber => $item->biblionumber } ); if ( $holds->count() ) { $return_data->{outstanding} = $holds->count(); @@ -1302,7 +1321,7 @@ sub checkHighHolds { } # Remove any items that are not holdable for this patron - @items = grep { CanItemBeReserved( $borrower->{borrowernumber}, $_->itemnumber, undef, { ignore_found_holds => 1 } )->{status} eq 'OK' } @items; + @items = grep { CanItemBeReserved( $patron , $_, undef, { ignore_found_holds => 1 } )->{status} eq 'OK' } @items; my $items_count = scalar @items; @@ -1317,22 +1336,22 @@ sub checkHighHolds { my $issuedate = dt_from_string(); - my $itype = $item_object->effective_itemtype; + my $itype = $item->effective_itemtype; my $daysmode = Koha::CirculationRules->get_effective_daysmode( { - categorycode => $borrower->{categorycode}, + categorycode => $patron->categorycode, itemtype => $itype, branchcode => $branchcode, } ); my $calendar = Koha::Calendar->new( branchcode => $branchcode, days_mode => $daysmode ); - my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branchcode, $borrower ); + my $orig_due = C4::Circulation::CalcDateDue( $issuedate, $itype, $branchcode, $patron->unblessed ); my $rule = Koha::CirculationRules->get_effective_rule( { - categorycode => $borrower->{categorycode}, - itemtype => $item_object->effective_itemtype, + categorycode => $patron->categorycode, + itemtype => $item->effective_itemtype, branchcode => $branchcode, rule_name => 'decreaseloanholds', } @@ -1345,7 +1364,7 @@ sub checkHighHolds { } else { $duration = C4::Context->preference('decreaseLoanHighHoldsDuration'); } - my $reduced_datedue = $calendar->addDate( $issuedate, $duration ); + my $reduced_datedue = $calendar->addDuration( $issuedate, $duration ); $reduced_datedue->set_hour($orig_due->hour); $reduced_datedue->set_minute($orig_due->minute); $reduced_datedue->truncate( to => 'minute' ); @@ -1473,6 +1492,8 @@ sub AddIssue { my ( $allowed, $message ) = CanBookBeReturned( $item_unblessed, C4::Context->userenv->{branch} ); return unless $allowed; AddReturn( $item_object->barcode, C4::Context->userenv->{'branch'} ); + # AddReturn certainly has side-effects, like onloan => undef + $item_object->discard_changes; } C4::Reserves::MoveReserve( $item_object->itemnumber, $borrower->{'borrowernumber'}, $cancelreserve ); @@ -1545,6 +1566,7 @@ sub AddIssue { )->store; } $issue->discard_changes; + C4::Auth::track_login_daily( $borrower->{userid} ); if ( $item_object->location && $item_object->location eq 'CART' && ( !$item_object->permanent_location || $item_object->permanent_location ne 'CART' ) ) { ## Item was moved to cart via UpdateItemLocationOnCheckin, anything issued should be taken off the cart. @@ -1610,7 +1632,7 @@ sub AddIssue { } # Record the fact that this book was issued. - &UpdateStats( + C4::Stats::UpdateStats( { branch => C4::Context->userenv->{'branch'}, type => ( $onsite_checkout ? 'onsite_checkout' : 'issue' ), @@ -1798,9 +1820,10 @@ branch and item type, regardless of patron category. The return value is a hashref containing the following keys: holdallowed => Hold policy for this branch and itemtype. Possible values: - 0: No holds allowed. - 1: Holds allowed only by patrons that have the same homebranch as the item. - 2: Holds allowed from any patron. + not_allowed: No holds allowed. + from_home_library: Holds allowed only by patrons that have the same homebranch as the item. + from_any_library: Holds allowed from any patron. + from_local_hold_group: Holds allowed from libraries in hold group returnbranch => branch to which to return item. Possible values: noreturn: do not return, let item remain where checked in (floating collections) @@ -1825,22 +1848,22 @@ sub GetBranchItemRule { my $holdallowed_rule = Koha::CirculationRules->get_effective_rule( { branchcode => $branchcode, - itemtype => $itemtype, - rule_name => 'holdallowed', + itemtype => $itemtype, + rule_name => 'holdallowed', } ); my $hold_fulfillment_policy_rule = Koha::CirculationRules->get_effective_rule( { branchcode => $branchcode, - itemtype => $itemtype, - rule_name => 'hold_fulfillment_policy', + itemtype => $itemtype, + rule_name => 'hold_fulfillment_policy', } ); my $returnbranch_rule = Koha::CirculationRules->get_effective_rule( { branchcode => $branchcode, - itemtype => $itemtype, - rule_name => 'returnbranch', + itemtype => $itemtype, + rule_name => 'returnbranch', } ); @@ -1848,7 +1871,7 @@ sub GetBranchItemRule { my $rules; $rules->{holdallowed} = defined $holdallowed_rule ? $holdallowed_rule->rule_value - : 2; + : 'from_any_library'; $rules->{hold_fulfillment_policy} = defined $hold_fulfillment_policy_rule ? $hold_fulfillment_policy_rule->rule_value : 'any'; @@ -1986,13 +2009,16 @@ sub AddReturn { my $borrowernumber = $patron ? $patron->borrowernumber : undef; # we don't know if we had a borrower or not my $patron_unblessed = $patron ? $patron->unblessed : {}; - my $update_loc_rules = get_yaml_pref_hash('UpdateItemLocationOnCheckin'); + my $update_loc_rules = Koha::Config::SysPrefs->find('UpdateItemLocationOnCheckin')->get_yaml_pref_hash(); map { $update_loc_rules->{$_} = $update_loc_rules->{$_}[0] } keys %$update_loc_rules; #We can only move to one location so we flatten the arrays if ($update_loc_rules) { if (defined $update_loc_rules->{_ALL_}) { if ($update_loc_rules->{_ALL_} eq '_PERM_') { $update_loc_rules->{_ALL_} = $item->permanent_location; } if ($update_loc_rules->{_ALL_} eq '_BLANK_') { $update_loc_rules->{_ALL_} = ''; } - if ( defined $item->location && $item->location ne $update_loc_rules->{_ALL_}) { + if ( + ( defined $item->location && $item->location ne $update_loc_rules->{_ALL_}) || + (!defined $item->location && $update_loc_rules->{_ALL_} ne "") + ) { $messages->{'ItemLocationUpdated'} = { from => $item->location, to => $update_loc_rules->{_ALL_} }; $item->location($update_loc_rules->{_ALL_})->store({skip_record_index=>1}); } @@ -2014,7 +2040,7 @@ sub AddReturn { if ($yaml) { $yaml = "$yaml\n\n"; # YAML is anal on ending \n. Surplus does not hurt my $rules; - eval { $rules = YAML::Load($yaml); }; + eval { $rules = YAML::XS::Load(Encode::encode_utf8($yaml)); }; if ($@) { warn "Unable to parse UpdateNotForLoanStatusOnCheckin syspref : $@"; } @@ -2102,51 +2128,68 @@ sub AddReturn { if ($item_was_lost) { $messages->{'WasLost'} = 1; unless ( C4::Context->preference("BlockReturnOfLostItems") ) { - $messages->{'LostItemFeeRefunded'} = $updated_item->{_refunded}; - $messages->{'LostItemFeeRestored'} = $updated_item->{_restored}; - - if ( $updated_item->{_charge} ) { - $issue //= Koha::Old::Checkouts->search( - { itemnumber => $item->itemnumber }, - { order_by => { '-desc' => 'returndate' }, rows => 1 } ) - ->single; - unless ( exists( $patron_unblessed->{branchcode} ) ) { - my $patron = $issue->patron; - $patron_unblessed = $patron->unblessed; - } - _CalculateAndUpdateFine( - { - issue => $issue, - item => $item->unblessed, - borrower => $patron_unblessed, - return_date => $return_date + my @object_messages = @{ $updated_item->object_messages }; + for my $message (@object_messages) { + $messages->{'LostItemFeeRefunded'} = 1 + if $message->message eq 'lost_refunded'; + $messages->{'LostItemFeeRestored'} = 1 + if $message->message eq 'lost_restored'; + + if ( $message->message eq 'lost_charge' ) { + $issue //= Koha::Old::Checkouts->search( + { itemnumber => $item->itemnumber }, + { order_by => { '-desc' => 'returndate' }, rows => 1 } + )->single; + unless ( exists( $patron_unblessed->{branchcode} ) ) { + my $patron = $issue->patron; + $patron_unblessed = $patron->unblessed; } - ); - _FixOverduesOnReturn( $patron_unblessed->{borrowernumber}, - $item->itemnumber, undef, 'RETURNED' ); - $messages->{'LostItemFeeCharged'} = 1; + _CalculateAndUpdateFine( + { + issue => $issue, + item => $item->unblessed, + borrower => $patron_unblessed, + return_date => $return_date + } + ); + _FixOverduesOnReturn( $patron_unblessed->{borrowernumber}, + $item->itemnumber, undef, 'RETURNED' ); + $messages->{'LostItemFeeCharged'} = 1; + } } } } # check if we have a transfer for this document - my ($datesent,$frombranch,$tobranch) = GetTransfers( $item->itemnumber ); + my $transfer = $item->get_transfer; # if we have a transfer to complete, we update the line of transfers with the datearrived - my $is_in_rotating_collection = C4::RotatingCollections::isItemInAnyCollection( $item->itemnumber ); - if ($datesent) { - # At this point we will either fill the transfer or it is a wrong transfer - # either way we should not now generate a new transfer + if ($transfer) { $validTransfer = 0; - if ( $tobranch eq $branch ) { - my $sth = C4::Context->dbh->prepare( - "UPDATE branchtransfers SET datearrived = now() WHERE itemnumber= ? AND datearrived IS NULL" - ); - $sth->execute( $item->itemnumber ); - $messages->{'TransferArrived'} = $frombranch; - } else { - $messages->{'WrongTransfer'} = $tobranch; - $messages->{'WrongTransferItem'} = $item->itemnumber; + if ( $transfer->in_transit ) { + if ( $transfer->tobranch eq $branch ) { + $transfer->receive; + $messages->{'TransferArrived'} = $transfer->frombranch; + # validTransfer=1 allows us returning the item back if the reserve is cancelled + $validTransfer = 1 if $transfer->reason eq 'Reserve'; + } + else { + $messages->{'WrongTransfer'} = $transfer->tobranch; + $messages->{'WrongTransferItem'} = $item->itemnumber; + $messages->{'TransferTrigger'} = $transfer->reason; + } + } + else { + if ( $transfer->tobranch eq $branch ) { + $transfer->receive; + $messages->{'TransferArrived'} = $transfer->frombranch; + # validTransfer=1 allows us returning the item back if the reserve is cancelled + $validTransfer = 1 if $transfer->reason eq 'Reserve'; + } + else { + $messages->{'WasTransfered'} = $transfer->tobranch; + $messages->{'TransferTrigger'} = $transfer->reason; + } } } @@ -2195,7 +2238,7 @@ sub AddReturn { } # Record the fact that this book was returned. - UpdateStats({ + C4::Stats::UpdateStats({ branch => $branch, type => $stat_type, itemnumber => $itemnumber, @@ -2220,6 +2263,7 @@ sub AddReturn { item => $item->unblessed, borrower => $patron->unblessed, branch => $branch, + issue => $issue }); } @@ -2237,16 +2281,21 @@ sub AddReturn { } # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer - if ($validTransfer && !$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) ){ + if ( $validTransfer && !C4::RotatingCollections::isItemInAnyCollection( $item->itemnumber ) + && ( $doreturn or $messages->{'NotIssued'} ) + and !$resfound + and ( $branch ne $returnbranch ) + and not $messages->{'WrongTransfer'} + and not $messages->{'WasTransfered'} ) + { my $BranchTransferLimitsType = C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ? 'effective_itemtype' : 'ccode'; if (C4::Context->preference("AutomaticItemReturn" ) or (C4::Context->preference("UseBranchTransferLimits") and ! IsBranchTransferAllowed($branch, $returnbranch, $item->$BranchTransferLimitsType ) )) { - $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s, %s)", $item->itemnumber,$branch, $returnbranch, $transfer_trigger; - $debug and warn "item: " . Dumper($item->unblessed); ModItemTransfer($item->itemnumber, $branch, $returnbranch, $transfer_trigger, { skip_record_index => 1 }); - $messages->{'WasTransfered'} = 1; + $messages->{'WasTransfered'} = $returnbranch; + $messages->{'TransferTrigger'} = $transfer_trigger; } else { $messages->{'NeedsTransfer'} = $returnbranch; $messages->{'TransferTrigger'} = $transfer_trigger; @@ -2466,7 +2515,7 @@ sub _calculate_new_debar_dt { branchcode => $branchcode, days_mode => 'Calendar' ); - $new_debar_dt = $calendar->addDate( $return_date, $suspension_days ); + $new_debar_dt = $calendar->addDuration( $return_date, $suspension_days ); } else { $new_debar_dt = $return_date->clone()->add_duration($suspension_days); @@ -2569,7 +2618,7 @@ sub _FixOverduesOnReturn { } ); - $credit->apply({ debits => [ $accountline ], offset_type => 'Forgiven' }); + $credit->apply({ debits => [ $accountline ] }); if (C4::Context->preference("FinesLog")) { &logaction("FINES", 'MODIFY',$borrowernumber,"Overdue forgiven: item $item"); @@ -2699,8 +2748,6 @@ already renewed the loan. $error will contain the reason the renewal can not pro sub CanBookBeRenewed { my ( $borrowernumber, $itemnumber, $override_limit, $cron ) = @_; - my $dbh = C4::Context->dbh; - my $renews = 1; my $auto_renew = "no"; my $item = Koha::Items->find($itemnumber) or return ( 0, 'no_item' ); @@ -2720,10 +2767,7 @@ sub CanBookBeRenewed { branchcode => $branchcode, rules => [ 'renewalsallowed', - 'no_auto_renewal_after', - 'no_auto_renewal_after_hard_limit', 'lengthunit', - 'norenewalbefore', 'unseen_renewals_allowed' ] } @@ -2749,82 +2793,21 @@ sub CanBookBeRenewed { return ( 0, 'overdue'); } - if ( $issue->auto_renew && $patron->autorenew_checkouts ) { - - if ( $patron->category->effective_BlockExpiredPatronOpacActions and $patron->is_expired ) { - return ( 0, 'auto_account_expired' ); - } - - if ( defined $issuing_rule->{no_auto_renewal_after} - and $issuing_rule->{no_auto_renewal_after} ne "" ) { - # Get issue_date and add no_auto_renewal_after - # If this is greater than today, it's too late for renewal. - my $maximum_renewal_date = dt_from_string($issue->issuedate, 'sql'); - $maximum_renewal_date->add( - $issuing_rule->{lengthunit} => $issuing_rule->{no_auto_renewal_after} - ); - my $now = dt_from_string; - if ( $now >= $maximum_renewal_date ) { - return ( 0, "auto_too_late" ); - } - } - if ( defined $issuing_rule->{no_auto_renewal_after_hard_limit} - and $issuing_rule->{no_auto_renewal_after_hard_limit} ne "" ) { - # If no_auto_renewal_after_hard_limit is >= today, it's also too late for renewal - if ( dt_from_string >= dt_from_string( $issuing_rule->{no_auto_renewal_after_hard_limit} ) ) { - return ( 0, "auto_too_late" ); - } - } - - if ( C4::Context->preference('OPACFineNoRenewalsBlockAutoRenew') ) { - my $fine_no_renewals = C4::Context->preference("OPACFineNoRenewals"); - my $amountoutstanding = - C4::Context->preference("OPACFineNoRenewalsIncludeCredit") - ? $patron->account->balance - : $patron->account->outstanding_debits->total_outstanding; - if ( $amountoutstanding and $amountoutstanding > $fine_no_renewals ) { - return ( 0, "auto_too_much_oweing" ); - } - } - } - - if ( defined $issuing_rule->{norenewalbefore} - and $issuing_rule->{norenewalbefore} ne "" ) - { - - # Calculate soonest renewal by subtracting 'No renewal before' from due date - my $soonestrenewal = dt_from_string( $issue->date_due, 'sql' )->subtract( - $issuing_rule->{lengthunit} => $issuing_rule->{norenewalbefore} ); - - # Depending on syspref reset the exact time, only check the date - if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date' - and $issuing_rule->{lengthunit} eq 'days' ) - { - $soonestrenewal->truncate( to => 'day' ); - } - - if ( $soonestrenewal > dt_from_string() ) - { - $auto_renew = ($issue->auto_renew && $patron->autorenew_checkouts) ? "auto_too_soon" : "too_soon"; - } - elsif ( $issue->auto_renew && $patron->autorenew_checkouts ) { - $auto_renew = "ok"; - } - } - - # Fallback for automatic renewals: - # If norenewalbefore is undef, don't renew before due date. - if ( $issue->auto_renew && $auto_renew eq "no" && $patron->autorenew_checkouts ) { - my $now = dt_from_string; - if ( $now >= dt_from_string( $issue->date_due, 'sql' ) ){ - $auto_renew = "ok"; - } else { - $auto_renew = "auto_too_soon"; - } - } + $auto_renew = _CanBookBeAutoRenewed({ + patron => $patron, + item => $item, + branchcode => $branchcode, + issue => $issue + }); + return ( 0, $auto_renew ) if $auto_renew =~ 'auto_too_soon' && $cron; + # cron wants 'too_soon' over 'on_reserve' for performance and to avoid + # extra notices being sent. Cron also implies no override + return ( 0, $auto_renew ) if $auto_renew =~ 'auto_account_expired'; + return ( 0, $auto_renew ) if $auto_renew =~ 'auto_too_late'; + return ( 0, $auto_renew ) if $auto_renew =~ 'auto_too_much_oweing'; } - my ( $resfound, $resrec, undef ) = C4::Reserves::CheckReserves($itemnumber); + my ( $resfound, $resrec, $possible_reserves ) = C4::Reserves::CheckReserves($itemnumber); # If next hold is non priority, then check if any hold with priority (non_priority = 0) exists for the same biblionumber. if ( $resfound && $resrec->{non_priority} ) { @@ -2840,9 +2823,7 @@ sub CanBookBeRenewed { if ( $resfound && C4::Context->preference('AllowRenewalIfOtherItemsAvailable') ) { - my $schema = Koha::Database->new()->schema(); - - my $item_holds = $schema->resultset('Reserve')->search( { itemnumber => $itemnumber, found => undef } )->count(); + my $item_holds = Koha::Holds->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; @@ -2850,60 +2831,45 @@ sub CanBookBeRenewed { else { # Get all other items that could possibly fill reserves - my @itemnumbers = $schema->resultset('Item')->search( - { - biblionumber => $resrec->{biblionumber}, - onloan => undef, - notforloan => 0, - -not => { itemnumber => $itemnumber } - }, - { columns => 'itemnumber' } - )->get_column('itemnumber')->all(); + my $items = Koha::Items->search({ + biblionumber => $resrec->{biblionumber}, + onloan => undef, + notforloan => 0, + -not => { itemnumber => $itemnumber } + }); # 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 @borrowernumbers = map { $_->{borrowernumber} } @$possible_reserves; + my $patrons = Koha::Patrons->search({ + borrowernumber => { -in => \@borrowernumbers } + }); # 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; - my %patrons; - ITEM: foreach my $itemnumber (@itemnumbers) { - my $item = Koha::Items->find( $itemnumber ); - next if IsItemOnHoldAndFound( $itemnumber ); - for my $borrowernumber (@borrowernumbers) { - my $patron = $patrons{$borrowernumber} //= Koha::Patrons->find( $borrowernumber ); + ITEM: while ( my $item = $items->next ) { + next if IsItemOnHoldAndFound( $item->itemnumber ); + while ( my $patron = $patrons->next ) { next unless IsAvailableForItemLevelRequest($item, $patron); - next unless CanItemBeReserved($borrowernumber,$itemnumber); - - push @reservable, $itemnumber; + next unless CanItemBeReserved($patron,$item,undef,{ignore_hold_counts=>1})->{status} eq 'OK'; + push @reservable, $item->itemnumber; if (@reservable >= @borrowernumbers) { $resfound = 0; last ITEM; } last; } + $patrons->reset; } } } - if( $cron ) { #The cron wants to return 'too_soon' over 'on_reserve' - return ( 0, $auto_renew ) if $auto_renew =~ 'too_soon';#$auto_renew ne "no" && $auto_renew ne "ok"; - return ( 0, "on_reserve" ) if $resfound; # '' when no hold was found - } else { # For other purposes we want 'on_reserve' before 'too_soon' - return ( 0, "on_reserve" ) if $resfound; # '' when no hold was found - return ( 0, $auto_renew ) if $auto_renew =~ 'too_soon';#$auto_renew ne "no" && $auto_renew ne "ok"; + + return ( 0, "on_reserve" ) if $resfound; # '' when no hold was found + return ( 0, $auto_renew ) if $auto_renew =~ 'too_soon';#$auto_renew ne "no" && $auto_renew ne "ok"; + if ( GetSoonestRenewDate($borrowernumber, $itemnumber) > dt_from_string() ){ + return (0, "too_soon") unless $override_limit; } return ( 0, "auto_renew" ) if $auto_renew eq "ok" && !$override_limit; # 0 if auto-renewal should not succeed @@ -3030,12 +2996,16 @@ 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 = ( $issue->renewals || 0 ) + 1; - my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, unseen_renewals = ?, lastreneweddate = ? - WHERE borrowernumber=? - AND itemnumber=?" - ); + my $sth = $dbh->prepare("UPDATE issues SET date_due = ?, renewals = ?, unseen_renewals = ?, lastreneweddate = ? WHERE issue_id = ?"); - $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $unseen_renewals, $lastreneweddate, $borrowernumber, $itemnumber ); + eval{ + $sth->execute( $datedue->strftime('%Y-%m-%d %H:%M'), $renews, $unseen_renewals, $lastreneweddate, $issue->issue_id ); + }; + if( $sth->err ){ + Koha::Exceptions::Checkout::FailedRenewal->throw( + error => 'Update of issue# ' . $issue->issue_id . ' failed with error: ' . $sth->errstr + ); + } # Update the renewal count on the item, and tell zebra to reindex $renews = ( $item_object->renewals || 0 ) + 1; @@ -3090,7 +3060,7 @@ sub AddRenewal { } # Add the renewal to stats - UpdateStats( + C4::Stats::UpdateStats( { branch => $item_object->renewal_branchcode({branch => $branch}), type => 'renew', @@ -3219,7 +3189,6 @@ sub GetSoonestRenewDate { ); my $now = dt_from_string; - return $now unless $issuing_rule; if ( defined $issuing_rule->{norenewalbefore} and $issuing_rule->{norenewalbefore} ne "" ) @@ -3234,6 +3203,15 @@ sub GetSoonestRenewDate { $soonestrenewal->truncate( to => 'day' ); } return $soonestrenewal if $now < $soonestrenewal; + } elsif ( $itemissue->auto_renew && $patron->autorenew_checkouts ) { + # Checkouts with auto-renewing fall back to due date + my $soonestrenewal = dt_from_string( $itemissue->date_due ); + if ( C4::Context->preference('NoRenewalBeforePrecision') eq 'date' + and $issuing_rule->{lengthunit} eq 'days' ) + { + $soonestrenewal->truncate( to => 'day' ); + } + return $soonestrenewal; } return $now; } @@ -3345,19 +3323,19 @@ sub GetIssuingCharges { if ( my $item_data = $sth->fetchrow_hashref ) { $item_type = $item_data->{itemtype}; $charge = $item_data->{rentalcharge}; - # FIXME This should follow CircControl - my $branch = C4::Context::mybranch(); - my $patron = Koha::Patrons->find( $borrowernumber ); - my $discount = Koha::CirculationRules->get_effective_rule({ - categorycode => $patron->categorycode, - branchcode => $branch, - itemtype => $item_type, - rule_name => 'rentaldiscount' - }); - if ($discount) { - $charge = ( $charge * ( 100 - $discount->rule_value ) ) / 100; - } if ($charge) { + # FIXME This should follow CircControl + my $branch = C4::Context::mybranch(); + my $patron = Koha::Patrons->find( $borrowernumber ); + my $discount = Koha::CirculationRules->get_effective_rule({ + categorycode => $patron->categorycode, + branchcode => $branch, + itemtype => $item_type, + rule_name => 'rentaldiscount' + }); + if ($discount) { + $charge = ( $charge * ( 100 - $discount->rule_value ) ) / 100; + } $charge = sprintf '%.2f', $charge; # ensure no fractions of a penny returned } } @@ -3406,10 +3384,13 @@ sub GetTransfers { SELECT datesent, frombranch, tobranch, - branchtransfer_id + branchtransfer_id, + daterequested, + reason FROM branchtransfers WHERE itemnumber = ? AND datearrived IS NULL + AND datecancelled IS NULL '; my $sth = $dbh->prepare($query); $sth->execute($itemnumber); @@ -3434,6 +3415,8 @@ sub GetTransfersFromTo { FROM branchtransfers WHERE frombranch=? AND tobranch=? + AND datecancelled IS NULL + AND datesent IS NOT NULL AND datearrived IS NULL "; my $sth = $dbh->prepare($query); @@ -3446,24 +3429,6 @@ sub GetTransfersFromTo { return (@gettransfers); } -=head2 DeleteTransfer - - &DeleteTransfer($itemnumber); - -=cut - -sub DeleteTransfer { - my ($itemnumber) = @_; - return unless $itemnumber; - my $dbh = C4::Context->dbh; - my $sth = $dbh->prepare( - "DELETE FROM branchtransfers - WHERE itemnumber=? - AND datearrived IS NULL " - ); - return $sth->execute($itemnumber); -} - =head2 SendCirculationAlert Send out a C or C alert using the messaging system. @@ -3503,8 +3468,8 @@ B: sub SendCirculationAlert { my ($opts) = @_; - my ($type, $item, $borrower, $branch) = - ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch}); + my ($type, $item, $borrower, $branch, $issue) = + ($opts->{type}, $opts->{item}, $opts->{borrower}, $opts->{branch}, $opts->{issue}); my %message_name = ( CHECKIN => 'Item_Check_in', CHECKOUT => 'Item_Checkout', @@ -3514,7 +3479,23 @@ sub SendCirculationAlert { borrowernumber => $borrower->{borrowernumber}, message_name => $message_name{$type}, }); - my $issues_table = ( $type eq 'CHECKOUT' || $type eq 'RENEWAL' ) ? 'issues' : 'old_issues'; + + + my $tables = { + items => $item->{itemnumber}, + biblio => $item->{biblionumber}, + biblioitems => $item->{biblionumber}, + borrowers => $borrower, + branches => $branch, + }; + + # TODO: Currently, we need to pass an issue_id as identifier for old_issues, but still an itemnumber for issues. + # See C4::Letters:: _parseletter_sth + if( $type eq 'CHECKIN' ){ + $tables->{old_issues} = $issue->issue_id; + } else { + $tables->{issues} = $item->{itemnumber}; + } my $schema = Koha::Database->new->schema; my @transports = keys %{ $borrower_preferences->{transports} }; @@ -3532,17 +3513,9 @@ sub SendCirculationAlert { branchcode => $branch, message_transport_type => $mtt, lang => $borrower->{lang}, - tables => { - $issues_table => $item->{itemnumber}, - 'items' => $item->{itemnumber}, - 'biblio' => $item->{biblionumber}, - 'biblioitems' => $item->{biblionumber}, - 'borrowers' => $borrower, - 'branches' => $branch, - } + tables => $tables, ) or next; - $schema->storage->txn_begin; C4::Context->dbh->do(q|LOCK TABLE message_queue READ|) unless $do_not_lock; C4::Context->dbh->do(q|LOCK TABLE message_queue WRITE|) unless $do_not_lock; my $message = C4::Message->find_last_message($borrower, $type, $mtt); @@ -3554,7 +3527,6 @@ sub SendCirculationAlert { $message->update; } C4::Context->dbh->do(q|UNLOCK TABLES|) unless $do_not_lock; - $schema->storage->txn_commit; } return; @@ -3570,19 +3542,24 @@ This function validate the line of brachtransfer but with the wrong destination sub updateWrongTransfer { my ( $itemNumber,$waitingAtLibrary,$FromLibrary ) = @_; - my $dbh = C4::Context->dbh; -# first step validate the actual line of transfert . - my $sth = - $dbh->prepare( - "update branchtransfers set datearrived = now(),tobranch=?,comments='wrongtransfer' where itemnumber= ? AND datearrived IS NULL" - ); - $sth->execute($FromLibrary,$itemNumber); - -# second step create a new line of branchtransfer to the right location . - ModItemTransfer($itemNumber, $FromLibrary, $waitingAtLibrary); - -#third step changing holdingbranch of item - my $item = Koha::Items->find($itemNumber)->holdingbranch($FromLibrary)->store; + + # first step: cancel the original transfer + my $item = Koha::Items->find($itemNumber); + my $transfer = $item->get_transfer; + $transfer->set({ datecancelled => dt_from_string, cancellation_reason => 'WrongTransfer' })->store(); + + # second step: create a new transfer to the right location + my $new_transfer = $item->request_transfer( + { + to => $transfer->to_library, + reason => $transfer->reason, + comment => $transfer->comments, + ignore_limits => 1, + enqueue => 1 + } + ); + + return $new_transfer; } =head2 CalcDateDue @@ -3651,7 +3628,7 @@ sub CalcDateDue { $dur = DateTime::Duration->new( days => $loanlength->{$length_key}); } my $calendar = Koha::Calendar->new( branchcode => $branch, days_mode => $daysmode ); - $datedue = $calendar->addDate( $datedue, $dur, $loanlength->{lengthunit} ); + $datedue = $calendar->addDuration( $datedue, $dur, $loanlength->{lengthunit} ); if ($loanlength->{lengthunit} eq 'days') { $datedue->set_hour(23); $datedue->set_minute(59); @@ -3855,14 +3832,15 @@ sub LostItem{ #warn " $issues->{'borrowernumber'} / $itemnumber "; } - MarkIssueReturned($borrowernumber,$itemnumber,undef,$patron->privacy) if $mark_returned; + MarkIssueReturned($borrowernumber,$itemnumber,undef,$patron->privacy,$params) if $mark_returned; } - #When item is marked lost automatically cancel its outstanding transfers and set items holdingbranch to the transfer source branch (frombranch) - if (my ( $datesent,$frombranch,$tobranch ) = GetTransfers($itemnumber)) { - Koha::Items->find($itemnumber)->holdingbranch($frombranch)->store({ skip_record_index => $params->{skip_record_index} }); + # When an item is marked as lost, we should automatically cancel its outstanding transfers. + my $item = Koha::Items->find($itemnumber); + my $transfers = $item->get_transfers; + while (my $transfer = $transfers->next) { + $transfer->cancel({ reason => 'ItemLost', force => 1 }); } - my $transferdeleted = DeleteTransfer($itemnumber); } sub GetOfflineOperations { @@ -4290,6 +4268,84 @@ sub _CalculateAndUpdateFine { } } +sub _CanBookBeAutoRenewed { + my ( $params ) = @_; + my $patron = $params->{patron}; + my $item = $params->{item}; + my $branchcode = $params->{branchcode}; + my $issue = $params->{issue}; + + return "no" unless $issue->auto_renew && $patron->autorenew_checkouts; + + my $issuing_rule = Koha::CirculationRules->get_effective_rules( + { + categorycode => $patron->categorycode, + itemtype => $item->effective_itemtype, + branchcode => $branchcode, + rules => [ + 'no_auto_renewal_after', + 'no_auto_renewal_after_hard_limit', + 'lengthunit', + 'norenewalbefore', + ] + } + ); + + if ( $patron->category->effective_BlockExpiredPatronOpacActions and $patron->is_expired ) { + return 'auto_account_expired'; + } + + if ( defined $issuing_rule->{no_auto_renewal_after} + and $issuing_rule->{no_auto_renewal_after} ne "" ) { + # Get issue_date and add no_auto_renewal_after + # If this is greater than today, it's too late for renewal. + my $maximum_renewal_date = dt_from_string($issue->issuedate, 'sql'); + $maximum_renewal_date->add( + $issuing_rule->{lengthunit} => $issuing_rule->{no_auto_renewal_after} + ); + my $now = dt_from_string; + if ( $now >= $maximum_renewal_date ) { + return "auto_too_late"; + } + } + if ( defined $issuing_rule->{no_auto_renewal_after_hard_limit} + and $issuing_rule->{no_auto_renewal_after_hard_limit} ne "" ) { + # If no_auto_renewal_after_hard_limit is >= today, it's also too late for renewal + if ( dt_from_string >= dt_from_string( $issuing_rule->{no_auto_renewal_after_hard_limit} ) ) { + return "auto_too_late"; + } + } + + if ( C4::Context->preference('OPACFineNoRenewalsBlockAutoRenew') ) { + my $fine_no_renewals = C4::Context->preference("OPACFineNoRenewals"); + my $amountoutstanding = + C4::Context->preference("OPACFineNoRenewalsIncludeCredit") + ? $patron->account->balance + : $patron->account->outstanding_debits->total_outstanding; + if ( $amountoutstanding and $amountoutstanding > $fine_no_renewals ) { + return "auto_too_much_oweing"; + } + } + + if ( defined $issuing_rule->{norenewalbefore} + and $issuing_rule->{norenewalbefore} ne "" ) { + if ( GetSoonestRenewDate($patron->id, $item->id) > dt_from_string()) { + return "auto_too_soon"; + } else { + return "ok"; + } + } + + # Fallback for automatic renewals: + # If norenewalbefore is undef, don't renew before due date. + my $now = dt_from_string; + if ( $now >= dt_from_string( $issue->date_due, 'sql' ) ){ + return "ok"; + } else { + return "auto_too_soon"; + } +} + sub _item_denied_renewal { my ($params) = @_;