From bb45a720626ca44f496354d3533280d1227b0e05 Mon Sep 17 00:00:00 2001 From: Benjamin Rokseth Date: Sun, 9 Sep 2018 23:17:00 +0200 Subject: [PATCH] Add Core::Circulation This is the Circulation child of Core. It is a work in progress, but ideally all neccessary subfunctions should be overridable, and it should consist of a bare minimum of functionality for handling circulation. https://bugs.koha-community.org/show_bug.cgi?id=21327 --- Core/Circulation.pm | 912 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 912 insertions(+) create mode 100644 Core/Circulation.pm diff --git a/Core/Circulation.pm b/Core/Circulation.pm new file mode 100644 index 0000000..15600a3 --- /dev/null +++ b/Core/Circulation.pm @@ -0,0 +1,912 @@ +package Core::Circulation; + +use strict; +use warnings; + +use Carp; +use Data::Dumper; +use Try::Tiny; + +use Koha::Items; +use Koha::Account; + +use Core::Exceptions; +use parent qw(Core::Main Core::Prefs); + +=head + Constructor + object args: + item : item object + library : library object + patron : patron object + prefs : preferences override hashmap + object accessors: + checkout : checkout object + messages : legacy messages hashmap (for ui/SIP) + iteminfo : legacy iteminfo hashmap (for ui/SIP) +=cut +sub new { + my ($class, $args) = @_; + ($args->{item} and $args->{library}) or die "Missing required arg item or library"; + + my $self = $class->SUPER::new(); + $self = { %$self, # Inherent anything from parent + error => undef, # Error return + item => $args->{item}, # Mandatory + library => $args->{library}, # Mandatory + patron => $args->{patron}, # Mandatory for checkouts + }; + $self->{library} or die "Should not proceed with circulation without library branch!"; + + # # Default prefs with overrides from args + $self->{prefs} = Core::Prefs->new($args->{prefs}); # TODO: Is this cached? If not, we should + $self->{checkout} = Koha::Checkouts->find({itemnumber => $self->{item}->itemnumber}); + $self->{messages} = {}; + $self->{iteminfo} = $self->{item}->unblessed; + + bless $self, $class; + + return $self; +} + +# Dummy override for testing +sub testOverride { + print "TEST: Core::Circulation called without overrides"; +} + +=head + Checkin + optional args: returnDate +=cut +sub Checkin { + my ($self, $returnDate) = @_; + warn "Checking in item " . $self->{item}->itemnumber . "\n"; + my $t0 = Time::HiRes::time(); + $returnDate ||= _today(); + + $self->testOverride(); + + # CHECKOUT + if ($self->{checkout}) { + $self->{patron} = $self->{checkout}->patron; + $self->{iteminfo} = { %{$self->{iteminfo}}, %{$self->{checkout}->unblessed} }; + $self->{iteminfo}->{overdue} = $self->{checkout}->is_overdue; + + if ($self->{item}->itemlost) { + $self->{messages}->{WasLost} = 1; + if (Koha::RefundLostItemFeeRules->should_refund({ + current_branch => $self->{library}->branchcode, + item_home_branch => $self->{item}->homebranch, + item_holding_branch => $self->{item}->holdingbranch, + })) { + $self->fixAccountForLostAndReturned(); + $self->{messages}->{Itemlost} = $self->{item}->barcode; + $self->{messages}->{LostItemFeeRefunded} = 1; + } + } + $self->fixOverduesOnReturn(); + } else { # NOT CHECKED OUT, JUST FIX STATUSES, TRANSFERS AND RETURN + $self->{error} = Core::Exception::Circulation::CheckinError->new(error => "Unable to checkin\n"); + if ($self->{item}->withdrawn) { + $self->{messages}->{Withdrawn} = $self->{item}->barcode; + } else { + $self->{messages}->{NotIssued} = $self->{item}->barcode; + } + } + + # TRANSFER + my $transfer = $self->{item}->get_transfer; + if ($transfer) { + if ($transfer->tobranch eq $self->{library}->branchcode) { + # ARRIVED AT RIGHT BRANCH + $transfer->datearrived(_today())->store; + } else { + # WRONG BRANCH - Should we fix transfers at once? + $self->{messages}->{WrongTransfer} = $transfer->tobranch; + $self->{messages}->{WrongTransferItem} = $self->{item}->itemnumber; + } + } + + # HOLD + my $hold = $self->getPendingHold; + if ($hold) { + if ($hold->branchcode ne $self->{library}->branchcode) { + $self->{messages}->{NeedsTransfer} = $hold->branchcode; + } + # Weird message return yes, but this is how Koha templates expect it + $self->{messages}->{ResFound} = { + %{$hold->unblessed}, + ResFound => sub {$hold->is_waiting ? return "Waiting" : return "Reserved"}, + }; + } + + # WRONG BRANCH - CREATE TRANSFER IF NOT ONE ALREADY + if ($self->{library}->branchcode ne $self->{item}->homebranch and not $transfer) { + Koha::Item::Transfer->new({ + itemnumber => $self->{item}->itemnumber, + frombranch => $self->{library}->branchcode, + tobranch => $self->{item}->homebranch, + datesent => _today(), + })->store; + $self->{messages}->{WasTransfered} = 1; + # IF AutomaticItemReturn false - set NeedsTransfer + $self->{prefs}->{AutomaticItemReturn} and $self->{messages}->{NeedsTransfer} = $self->{item}->homebranch; + } + + # TODO: Rewrite to Core? + C4::Stats::UpdateStats({ + branch => $self->{library}->branchcode, + type => "return", + itemnumber => $self->{item}->itemnumber, + itemtype => $self->{item}->itype, + borrowernumber => $self->{patron} ? $self->{patron}->borrowernumber : undef, + ccode => $self->{item}->ccode, + }); + + # TODO: Borrower message (SendCirculationAlert) or HANDLE IN PrintSlip ? + $self->sendCirculationAlert(); + + $self->removeOverdueDebarments(); + + # RETURN + if ($self->{checkout}) { + $self->{checkout}->returndate($returnDate)->store; + if ($self->{checkout}->patron->privacy == 2) { + $self->{checkout}->borrowernumber(undef)->store; + } else { + $self->{patron} and $self->{item}->last_returned_by( $self->{patron} ); + } + # MERGE TABLES - PLEASE + my $old_checkout = Koha::Old::Checkout->new($self->{checkout}->unblessed)->store; + $self->{checkout}->delete; + # TODO: Needs to decide if $self->{checkout} should be emptied upon checkout or not + # As templates require checkout object we must leave it for now + # undef $self->{checkout}; + $self->{messages}->{WasReturned} = 1; + } else { + $self->{error} = Core::Exception::Circulation::CheckinError->new(error => "Unable to checkin\n"); + $self->{messages}->{WasReturned} = 0; + $self->{messages}->{DataCorrupted} = 1; + } + + # TODO: rewrite all methods to overridable circulation methods + # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer + my $returnbranch = $self->getBranchItemRule(); + my $is_in_rotating_collection = C4::RotatingCollections::isItemInAnyCollection( $self->{item}->itemnumber ); + + if (!$is_in_rotating_collection && $self->{messages}->{NotIssued} and !$hold and ($self->{item}->homebranch ne $returnbranch) and not $self->{messages}->{WrongTransfer}) { + if ( $self->{prefs}->{AutomaticItemReturn} or ( $self->{prefs}->{UseBranchTransferLimits} + and ! C4::Circulation::IsBranchTransferAllowed($self->{item}->homebranch, $returnbranch, $self->{item}->{$self->{prefs}->{BranchTransferLimitsType}} ) + )) { + # AutomaticItemReturn: transfer to returnbranch + $transfer and $transfer->set({ + datearrived => \"NOW()", + comments => undef, + }); + Koha::Item::Transfer->new({ + itemnumber => $self->{item}->itemnumber, + frombranch => $self->{library}->branchcode, + tobranch => $returnbranch, + datesent => _today(), + })->store; + $self->{prefs}->{ReturnToShelvingCart} and $self->cartToShelf(); + $self->{messages}->{WasTransfered} = 1; + } else { + $self->{messages}->{NeedsTransfer} = $returnbranch; + } + } + + # UPDATE ITEM + $self->{item}->set({ + holdingbranch => $returnbranch ? $returnbranch : $self->{library}->branchcode, + onloan => undef, + datelastseen => _today(), + itemlost => 0, + })->store; + + my $dt = Time::HiRes::time() - $t0; + printf STDERR "[%s] Checkin called : %s - elapsed %.3fs\n", ref $self, scalar(gmtime()), $dt; + return $self; +} + +# IN -> $self, $dateDue[, $cancelReserve, $issueDate, $sipMode] +sub Checkout { + my ($self, $dateDue, $cancelReserve, $issueDate, $sipMode, $params) = @_; # Surely most params can be skipped here + $self->{patron} and $self->{library} or Core::Exception::Circulation::CheckinError->throw(error => "No checkout without patron and/or library!\n"); + + # extra 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 $t0 = Time::HiRes::time(); + warn "Checking out item " . $self->{item}->barcode . " for patron " . $self->{patron}->borrowernumber . "\n"; + $issueDate ||= _today(); + + # PREFS THAT APPLY + # AllowReturnToBranch + my $circControlBranch = $self->getCircControlBranch(); + + # ALREADY CHECKED OUT? + if ($self->{checkout}) { + if ($self->{checkout}->patron->borrowernumber eq $self->{patron}->borrowernumber) { + if ($self->canItemBeRenewed() and not $switch_onsite_checkout) { + $self->Renew( $dateDue ); + } + } else { + # Wrong checkout, return first? + if ($self->canItemBeReturned() and not $switch_onsite_checkout) { + $self->Checkin(); + } + } + } else { + + # TRANSFERS AND HOLDS + my $transfer = $self->{item}->get_transfer; + if ($transfer) { + if ($transfer->tobranch eq $self->{library}->branchcode) { + $transfer->datearrived(_today())->store; # Finish transfer + } else { + $transfer->tobranch($self->{library}->branchcode)->store; # Update transfer + } + } + + # TODO: C4::Circulation.pm:1317 + # MoveReserve( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $cancelreserve ); + # -> C4::Reserves.pm:1712 + # -> CheckReserves C4::Reserves.pm:626 + + # HOLD + my $hold = $self->getPendingHold(); + if ($hold) { + # Right patron - fill hold + if ($hold->borrowernumber == $self->{patron}->borrowernumber) { + $hold->set({found => "F", priority => 0}); + # Merge tables - PLEASE! + Koha::Old::Hold->new( $hold->unblessed() )->store(); + $hold->delete(); + } else { + # TODO: Checkout should be aborted here? + $self->{error} = Core::Exception::Circulation::CheckoutError->new("Unable to checkout - hold for another patron!"); + } + } + + # CIRCULATION RULES + my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule({ + categorycode => $self->{patron}->{categorycode}, + itemtype => $self->{item}->itype, + branchcode => $self->{library}->branchcode, + }); + + # CHECKOUT + # TODO: rewrite calculate dateDue to overridable module + unless ($dateDue) { + my $itype = $self->{item}->effective_itemtype; + $dateDue = C4::Circulation::CalcDateDue( $issueDate, $itype, $self->{library}->branchcode, $self->{patron}->unblessed, 1 ); + + } + $dateDue->truncate( to => "minute" ); + + # TODO: auto_renew and onsite_checkout + $self->{checkout} = Koha::Checkout->new({ + borrowernumber => $self->{patron}->borrowernumber, + itemnumber => $self->{item}->itemnumber, + issuedate => $issueDate, + date_due => $dateDue, + branchcode => $self->{library}->branchcode, + })->store; + + # Why was totalissues never set? + my $totalissues = $self->{item}->biblioitem->totalissues // 0; + $self->{item}->biblioitem->totalissues($totalissues + 1); + + # LOST ITEM REFUND + if ($self->{item}->itemlost) { + # REFUND LOST ITEM FEE + if (Koha::RefundLostItemFeeRules->should_refund({ + current_branch => $self->{library}->branchcode, + item_home_branch => $self->{item}->homebranch, + item_holding_branch => $self->{item}->holdingbranch, + })) { + $self->fixAccountForLostAndReturned(); + } + } + + # UPDATE ITEM + $self->{item}->set({ + issues => $self->{item}->issues + 1, + holdingbranch => $self->{library}->branchcode, + itemlost => 0, + onloan => $dateDue->ymd(), + datelastborrowed => _today(), + datelastseen => _today(), + })->store; + + C4::Stats::UpdateStats({ + branch => $self->{library}->branchcode, + type => "issue", + other => ( $sipMode ? "SIP-$sipMode" : "" ), + itemnumber => $self->{item}->itemnumber, + itemtype => $self->{item}->itype, + location => $self->{item}->location, + borrowernumber => $self->{patron}->borrowernumber, + ccode => $self->{item}->ccode, + }); + + # TODO: Borrower message (SendCirculationAlert) HANDLE IN PrintSlip ? + $self->sendCirculationAlert(); + + $self->removeOverdueDebarments(); + } + my $dt = Time::HiRes::time() - $t0; + printf STDERR "[%s] Checkout called : %s - elapsed %.3fs\n", ref $self, scalar(gmtime()), $dt; + return $self; + +} + +sub Renew { + my ($self, $dateDue, $lastRenewedDate) = @_; # Probably can skip most of these + $self->{patron} and $self->{library} or die "Do we really need patron and library?"; + warn "Renewing item " . $self->{item}->itemnumber . " for patron " . $self->{patron}->borrowernumber . "\n"; + my $t0 = Time::HiRes::time(); + + $self->{checkout} or die "No checkout, cannot renew"; + $lastRenewedDate ||= _today(); + + # PREFS THAT APPLY + # CalculateFinesOnReturn + # RenewalPeriodBase + # RenewalSendNotice + # CircControl + + $self->fixOverduesOnReturn(); + $self->canItemBeRenewed() or return $self; + + # default to checkout branch + my $circControlBranch = $self->getCircControlBranch(); + + # 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. + if ( defined $dateDue && ref $dateDue ne "DateTime" ) { + carp "Invalid date passed to Renew."; + return; + } + # TODO: Adjust to use calendar + unless ($dateDue) { + my $itemType = $self->{item}->effective_itemtype; + if ($self->{prefs}->{RenewalPeriodBase} eq "date_due") { + $dateDue = _mysqldate2dt($self->{checkout}->date_due); + } else { + $dateDue = _today(); + } + $dateDue = C4::Circulation::CalcDateDue($dateDue, $itemType, $circControlBranch, $self->{patron}->unblessed, 1); + } + + # UPDATE CHECKOUT AND ITEM + my $checkoutRenewals = $self->{checkout}->renewals || 0; + my $itemRenewals = $self->{item}->renewals || 0; + $self->{checkout}->set({ + date_due => _dt2mysqldate($dateDue), + lastreneweddate => $lastRenewedDate, + renewals => $checkoutRenewals + 1, + })->store; + + $self->{item}->set({ + renewals => $itemRenewals + 1, + onloan => _dt2mysqldate($dateDue), + })->store; + + # TODO: Renewal notice? + $self->removeOverdueDebarments(); + + # STATS + C4::Circulation::UpdateStats({ + branch => $self->{checkout}->branchcode, + type => "renew", + amount => undef, + itemnumber => $self->{item}->itemnumber, + itemtype => $self->{item}->itype, + location => $self->{item}->location, + borrowernumber => $self->{patron}->borrowernumber, + ccode => $self->{item}->ccode, + }); + my $dt = Time::HiRes::time() - $t0; + printf STDERR "[%s] Renew called : %s - elapsed %.3fs\n", ref $self, scalar(gmtime()), $dt; + return $self; +} + +=head + getBranchItemRule + use legacy C4::Circulation::GetBranchItemRule +=cut +sub getBranchItemRule { + my $self = shift; + use C4::Circulation; + my $hbr = C4::Circulation::GetBranchItemRule($self->{item}->homebranch, $self->{item}->itype)->{'returnbranch'} || "homebranch"; + return $self->{item}->{$hbr} || $self->{item}->homebranch ; +} + +=head2 $self->cartToShelf + + Set the current shelving location of the item record + to its stored permanent shelving location. This is + primarily used to indicate when an item whose current + location is a special processing ('PROC') or shelving cart + ('CART') location is back in the stacks. + +=cut + +sub cartToShelf { + my $self = shift; + if ($self->{item}->location eq "CART") { + $self->{item}->set({ location => $self->{item}->permanent_location })->store; + } + return $self; +} + +=head2 $self->getCircControlBranch + + Return the library code to be used to determine which circulation + policy applies to a transaction. Looks up the CircControl and + HomeOrHoldingBranch system preferences. + +=cut + +sub getCircControlBranch { + my $self = shift; + my $branch; + + if ($self->{prefs}->{CircControl} eq "PickupLibrary" and $self->{checkout}) { + $branch = $self->{checkout}->branchcode; # Why C4::Context->userenv->{'branch'} in C4::Circulation? + } elsif ($self->{prefs}->{CircControl} eq "PatronLibrary") { + $branch = $self->{patron}->branchcode; + } else { + my $branchfield = $self->{prefs}->{HomeOrHoldingBranch} || "homebranch"; + $branch = $self->{item}->{$branchfield} || $self->{item}->{homebranch}; + } + return $branch; +} + +=head $self->fixAccountForLostAndReturned + If item lost, refund and mark as lost returned +=cut +sub fixAccountForLostAndReturned { + my $self = shift; + my $acc = Koha::Account::Lines->search( + { + itemnumber => $self->{item}->itemnumber, + accounttype => { -in => [ "L", "Rep" ] }, + }, + { + order_by => { -desc => [ "date", "accountno" ] } + } + )->next(); + return unless $acc; + + $acc->accounttype("LR"); + $acc->store(); + + my $account = Koha::Account->new( { patron_id => $acc->borrowernumber } ); + my $credit_id = $account->pay( + { + amount => $acc->amount, + description => "Item Returned " . $self->{item}->barcode, + account_type => "CR", + offset_type => "Lost Item Return", + accounlines => [$acc], + + } + ); + + return $credit_id; +} + +=head + ref C4::Circulation.pm L2369 + From what I could decipher, this sets fine accounttype to F in normal cases, rest is handled in cronjobs + TODO: + if params exemptfine -> add line with credit and type "Forgiven" + if params dropbox -> add line with credit and type "Dropbox" + +=cut +sub fixOverduesOnReturn { + my $self = shift; + + $self->{patron} and $self->{item} or croak "Missing patron or item"; + + my $accountline = Koha::Account::Lines->search( + { + borrowernumber => $self->{patron}->borrowernumber, + itemnumber => $self->{item}->itemnumber, + -or => [ + accounttype => "FU", + accounttype => "O", + ], + } + )->next(); + $accountline and $accountline->accounttype("F")->store; + return $self; +} + +=head + ref C4::Circulation L676 + +=cut +sub canItemBeIssued { + my ($self, $dateDue, $ignoreReserves) = @_; + # 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; + + # PREFS + # noissuescharge + # AllowFineOverride + # AllFinesNeedOverride + # NoIssuesChargeGuarantees + # This should be returned before calling Core::Circulation + $self->{item}->itemnumber or return ($self, {UNKNOWN_BARCODE => 1}); + # TODO: should dateDue be automatically calculated? + # Makes no sense to ask for dateDue +# unless ($dateDue) { +# my $itype = $self->{item}->effective_itemtype; +# $dateDue = C4::Circulation::CalcDateDue( _today(), $itype, $self->{library}->branchcode, $self->{patron}->unblessed, 1 ); + +# } +# $dateDue->truncate( to => "minute" ); + + # PATRON FLAGS + $self->{patron}->gonenoaddress and $issuingimpossible->{GNA} = 1; + $self->{patron}->lost and $issuingimpossible->{CARD_LOST} = 1; + $self->{patron}->debarred and $issuingimpossible->{DEBARRED} = 1; + $self->{patron}->is_expired and $issuingimpossible->{EXPIRED} = 1; + + # DEBTS + # Note: Koha::Account is different than other Koha Objects + my $debt = Koha::Account->new({patron_id => $self->{patron}->borrowernumber})->balance; + if ($debt > 0) { + # TODO: if $self->{prefs}->{IssuingInProcess} + if ($debt > $self->{prefs}->{noissuescharge} and not $self->{prefs}->{AllowFineOverride}) { + $issuingimpossible->{DEBT} = sprintf( "%.2f", $debt ); + } + } elsif ($self->{prefs}->{NoIssuesChargeGuarantees}) { + my $guarantees_charges; + foreach my $g ( @$self->{patron}->guarantees ) { + $guarantees_charges += Koha::Account->new({patron_id => $g->borrowernumber})->balance; + } + if ( $guarantees_charges > $self->{prefs}->{NoIssuesChargeGuarantees} and not $self->{prefs}->{AllowFineOverride}) { + $issuingimpossible->{DEBT_GUARANTEES} = sprintf( "%.2f", $$guarantees_charges ); + } + } + + # DEBARRED + if ( my $debarred_date = $self->{patron}->is_debarred ) { + if ($debarred_date eq "9999-12-31") { + $issuingimpossible->{USERBLOCKEDNOENDDATE} = $debarred_date; + } else { + $issuingimpossible->{USERBLOCKEDWITHENDDATE} = $debarred_date; + } + } elsif ( my $num_overdues = $self->{patron}->has_overdues ) { + if ( C4::Context->preference("OverduesBlockCirc") eq "block") { + $issuingimpossible->{USERBLOCKEDOVERDUE} = $num_overdues; + } + elsif ( C4::Context->preference("OverduesBlockCirc") eq "confirmation") { + $needsconfirmation->{USERBLOCKEDOVERDUE} = $num_overdues; + } + } + + # ALREADY CHECKED OUT + if ($self->{checkout}) { + # SAME PATRON + if ($self->{checkout}->borrowernumber eq $self->{patron}->borrowernumber ) { + if ( $self->{checkout}->onsite_checkout and $self->{prefs}->{SwitchOnSiteCheckouts}) { + $messages->{ONSITE_CHECKOUT_WILL_BE_SWITCHED} = 1; + } else { + my ($ok, $error) = $self->canItemBeRenewed(); + if ($ok) { + if ( $error eq "onsite_checkout" ) { + $issuingimpossible->{NO_RENEWAL_FOR_ONSITE_CHECKOUTS} = 1; + } else { + $issuingimpossible->{NO_MORE_RENEWALS} = 1; + } + } else { + $needsconfirmation->{RENEW_ISSUE} = 1; + } + } + # DIFFERENT PATRON + } else { + my ( $ok, $message ) = $self->canItemBeReturned(); + if ( $ok ) { + $needsconfirmation->{ISSUED_TO_ANOTHER} = 1; + # THESE ARE JUST SILLY - should be removed + $needsconfirmation->{issued_firstname} = $self->{checkout}->patron->firstname; + $needsconfirmation->{issued_surname} = $self->{checkout}->patron->surname; + $needsconfirmation->{issued_cardnumber} = $self->{checkout}->patron->cardnumber; + $needsconfirmation->{issued_borrowernumber} = $self->{checkout}->patron->borrowernumber; + } else { + $issuingimpossible->{RETURN_IMPOSSIBLE} = 1; + $issuingimpossible->{branch_to_return} = $message; + } + } + } + + # TODO: Too Many checkouts + # JB34 CHECKS IF BORROWERS DON'T HAVE ISSUE TOO MANY BOOKS + # + # my $switch_onsite_checkout = ( + # C4::Context->preference('SwitchOnSiteCheckouts') + # and $issue + # and $issue->onsite_checkout + # 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 && not exists $needsconfirmation->{RENEW_ISSUE} ) { + # 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 + if ($self->{patron}->wants_check_for_previous_checkout and $self->{patron}->do_check_for_previous_checkout($self->{item})) { + $needsconfirmation->{PREVISSUE} = 1 + } + + # ITEM BASED RESTRICTIONS + # TODO: + # Age restriction + # pref item-level_itypes + # independentBranches + # rental charges + # decreaseLoanHighHolds + + $self->{item}->withdrawn > 0 and $issuingimpossible->{WTHDRAWN} = 1; + if ($self->{item}->restricted and $self->{item}->restricted > 0) { + $issuingimpossible->{RESTRICTED} = 1; + } + + if ($self->{item}->notforloan) { + if ($self->{prefs}->{AllowNotForLoanOverride}) { + $issuingimpossible->{NOT_FOR_LOAN} = 1; + $issuingimpossible->{item_notforloan} = $self->{item}->notforloan; # WHY THIS? + } else { + $needsconfirmation->{NOT_FOR_LOAN_FORCING} = 1; + $needsconfirmation->{item_notforloan} = $self->{item}->notforloan; # WHY THIS? + } + } + + if ( $self->{item}->itemlost and $self->{prefs}->{IssueLostItem} ne "nothing" ) { + my $av = Koha::AuthorisedValues->search({ category => 'LOST', authorised_value => $self->{item}->itemlost }); + my $code = $av->count ? $av->next->lib : ""; + ( $self->{prefs}->{IssueLostItem} eq "confirm" ) and $needsconfirmation->{ITEM_LOST} = $code; + ( $self->{prefs}->{IssueLostItem} eq "alert" ) and $alerts->{ITEM_LOST} = $code; # TODO: alert only used once, should be removed + } + + unless ( $ignoreReserves ) { + my $hold = $self->getPendingHold(); + if ($hold) { + if ($hold->is_waiting) { + $needsconfirmation->{RESERVE_WAITING} = 1; + $needsconfirmation->{'reswaitingdate'} = $hold->waitingdate; + } else { + $needsconfirmation->{RESERVED} = 1; + } + + $needsconfirmation->{'resfirstname'} = $hold->patron->firstname; + $needsconfirmation->{'ressurname'} = $hold->patron->surname; + $needsconfirmation->{'rescardnumber'} = $hold->patron->cardnumber; + $needsconfirmation->{'resborrowernumber'} = $hold->patron->borrowernumber; + $needsconfirmation->{'resbranchcode'} = $hold->patron->branchcode; + $needsconfirmation->{'resreservedate'} = $hold->reservedate; + } + } + + # TODO: consider if this is neccessary + if ( not $self->{prefs}->{AllowMultipleIssuesOnABiblio} and not $issuingimpossible->{NO_MORE_RENEWALS} and not $needsconfirmation->{RENEW_ISSUE}) { + # don't do the multiple loans per bib check if we've + # already determined that we've got a loan on the same item + unless ($self->{item}->biblio->subscriptions->count) { + my $checkouts = Koha::Checkouts->search({ + borrowernumber => $self->{patron}->borrowernumber, + biblionumber => $self->{item}->biblionumber, + }, { + join => 'item', + }); + if ( $checkouts->count ) { + $needsconfirmation->{BIBLIO_ALREADY_ISSUED} = 1; + } + } + } + + return ( $issuingimpossible, $needsconfirmation, $alerts, $messages ); +} + +=head + $self->canItemBeRenewed + if no holds binds item, allow renewal + args: + override => bool + return: + bool => can/cannot be renewed + msg => legacy error msg string +=cut + +sub canItemBeRenewed { + my ($self, $override) = @_; + $self->{checkout} or return ( 0, "no_checkout" ); + + # PREFS THAT MIGHT APPLY + # OverduesBlockRenewing + # RestrictionBlockRenewing + + # Too few items to fill holds means item cannot be renewed + if (not $self->canItemFillHold and not $override) { + return ( 0, "on_hold" ); + } + + # CIRCULATION RULES + my $circ_rule = Koha::IssuingRules->get_effective_issuing_rule( + { categorycode => $self->{patron}->categorycode, + itemtype => $self->{item}->itype, + branchcode => $self->{checkout}->branchcode, + } + ); + + my $renewals = $self->{checkout}->renewals || 0; + $circ_rule or return ( 0, "too_many" ); # MISSING CIRC RULE - too_many in original, but should perhaps be better worded + $circ_rule->renewalsallowed <= $renewals or return ( 0, "too_many" ); + + + $self->{patron}->is_debarred and return ( 0, "restriction" ); + ($self->{patron}->has_overdues or $self->{checkout}->is_overdue) and return ( 0, "overdue" ); + + # TODO: AUTORENEW - USE CASE? + + # NO RENEWAL BEFORE + if ( defined $circ_rule->norenewalbefore and $circ_rule->norenewalbefore ne "" ) { + my $dt = _mysqldate2dt($self->{checkout}->date_due); + my $soonestrenewal = $dt->subtract( $circ_rule->lengthunit => $circ_rule->norenewalbefore ); + if ( $soonestrenewal > DateTime->now() ) { + return ( 0, "too_soon" ); + } + } + return (1, undef); +} + +sub canItemBeReturned { + my $self = shift; + my $allowreturntobranch = $self->{prefs}->{AllowReturnToBranch}; + + # assume return is allowed to start + my $allowed = 1; + my $message; + + # identify all cases where return is forbidden + if ($allowreturntobranch eq "homebranch" && $self->{library}->branchcode ne $self->{item}->homebranch) { + $allowed = 0; + $message = $self->{item}->homebranch; + } elsif ($allowreturntobranch eq "holdingbranch" && $self->{library}->branchcode ne $self->{item}->holdingbranch) { + $allowed = 0; + $message = $self->{item}->holdingbranch; + } elsif ($allowreturntobranch eq "homeorholdingbranch" && $self->{library}->branchcode ne $self->{item}->homebranch + && $self->{library}->branchcode ne $self->{item}->holdingbranch) { + $allowed = 0; + $message = $self->{item}->homebranch; # FIXME: choice of homebranch is arbitrary + } + + return ($allowed, $message); +} + +=head + Check if item can fill a pending hold + reservable means: + - item not found (Waiting or in Transfer) + - item not lost, withdrawn + - item notforloan not code > 0 + - item not damaged (unless AllowHoldsOnDamagedItems) + - number of reservable items is greater than number of pending holds + - something something onshelfholds + + ref: C4::Reserves::CanItemBeReserved L282 +=cut +sub canItemFillHold { + my $self = shift; + + # PREFS + # AllowRenewalIfOtherItemsAvailable + # item-level_itypes + # IndependentBranches + # canreservefromotherbranches + # AllowHoldsOnDamagedItems + + my $reservableItems = Koha::Items->search({ + "me.biblionumber" => $self->{item}->biblionumber, + itemlost => 0, + withdrawn => 0, + notforloan => { "<=" => 0 }, + "reserves.found" => { "!=" => undef }, + }, + { join => "reserves" }); + + if ($reservableItems->count < $self->getPendingHoldsByBiblio->count) { + return 0; + } else { + return 1; + } +} +=head + return next hold, item or biblio level +=cut +sub getPendingHold { + my $self = shift; + my $itemholds = $self->{item}->current_holds; + my $biblioholds = Koha::Holds->search({biblionumber => $self->{item}->biblionumber, found => undef, suspend => 0}, { order_by => { -asc => "priority" }}); + my $hold = $itemholds->count ? $itemholds->next : $biblioholds->count ? $biblioholds->next : undef; + return $hold; +} + +=head + return pending holds on biblio level +=cut +sub getPendingHoldsByBiblio { + my $self = shift; + return Koha::Holds->search({biblionumber => $self->{item}->biblionumber, found => undef, suspend => 0}, { order_by => { -asc => "priority" }}); +} + +sub _mysqldate2dt { + my $datestring = shift; + my $strp = DateTime::Format::Strptime->new( pattern => "%Y-%m-%d %H:%M:%S" ); + return $strp->parse_datetime( $datestring ); +} + +sub _dt2mysqldate { + my $dt = shift; + if (ref $dt ne "DateTime") { + return $dt; + } else { + return $dt->strftime("%F %T"); + } +} + +sub _ymd2dt { + my $ymdstring = shift; + my $strp = DateTime::Format::Strptime->new( pattern => "%Y-%m-%d" ); + return $strp->parse_datetime( $ymdstring ); +} + +sub _today { + return DateTime->now()->ymd; +} + +sub sendCirculationAlert { + # TODO +} + +=head + Remove any OVERDUES related debarment if the borrower has no overdues +=cut +sub removeOverdueDebarments { + my $self = shift; + return unless $self->{patron}; + if ( $self->{patron}->is_debarred + && ! $self->{patron}->has_overdues + && @{ Koha::Patron::Debarments::GetDebarments({ borrowernumber => $self->{patron}->borrowernumber, type => "OVERDUES" }) } + ) { + Koha::Patron::Debarments::DelUniqueDebarment({ borrowernumber => $self->{patron}->borrowernumber, type => "OVERDUES" }); + } +} + +1; -- 2.1.4