Line 0
Link Here
|
0 |
- |
1 |
package Core::Circulation; |
|
|
2 |
|
3 |
use strict; |
4 |
use warnings; |
5 |
|
6 |
use Carp; |
7 |
use Data::Dumper; |
8 |
use Try::Tiny; |
9 |
|
10 |
use Koha::Items; |
11 |
use Koha::Account; |
12 |
|
13 |
use Core::Exceptions; |
14 |
use parent qw(Core::Main Core::Prefs); |
15 |
|
16 |
=head |
17 |
Constructor |
18 |
object args: |
19 |
item : item object |
20 |
library : library object |
21 |
patron : patron object |
22 |
prefs : preferences override hashmap |
23 |
object accessors: |
24 |
checkout : checkout object |
25 |
messages : legacy messages hashmap (for ui/SIP) |
26 |
iteminfo : legacy iteminfo hashmap (for ui/SIP) |
27 |
=cut |
28 |
sub new { |
29 |
my ($class, $args) = @_; |
30 |
($args->{item} and $args->{library}) or die "Missing required arg item or library"; |
31 |
|
32 |
my $self = $class->SUPER::new(); |
33 |
$self = { %$self, # Inherent anything from parent |
34 |
error => undef, # Error return |
35 |
item => $args->{item}, # Mandatory |
36 |
library => $args->{library}, # Mandatory |
37 |
patron => $args->{patron}, # Mandatory for checkouts |
38 |
}; |
39 |
$self->{library} or die "Should not proceed with circulation without library branch!"; |
40 |
|
41 |
# # Default prefs with overrides from args |
42 |
$self->{prefs} = Core::Prefs->new($args->{prefs}); # TODO: Is this cached? If not, we should |
43 |
$self->{checkout} = Koha::Checkouts->find({itemnumber => $self->{item}->itemnumber}); |
44 |
$self->{messages} = {}; |
45 |
$self->{iteminfo} = $self->{item}->unblessed; |
46 |
|
47 |
bless $self, $class; |
48 |
|
49 |
return $self; |
50 |
} |
51 |
|
52 |
# Dummy override for testing |
53 |
sub testOverride { |
54 |
print "TEST: Core::Circulation called without overrides"; |
55 |
} |
56 |
|
57 |
=head |
58 |
Checkin |
59 |
optional args: returnDate |
60 |
=cut |
61 |
sub Checkin { |
62 |
my ($self, $returnDate) = @_; |
63 |
warn "Checking in item " . $self->{item}->itemnumber . "\n"; |
64 |
my $t0 = Time::HiRes::time(); |
65 |
$returnDate ||= _today(); |
66 |
|
67 |
$self->testOverride(); |
68 |
|
69 |
# CHECKOUT |
70 |
if ($self->{checkout}) { |
71 |
$self->{patron} = $self->{checkout}->patron; |
72 |
$self->{iteminfo} = { %{$self->{iteminfo}}, %{$self->{checkout}->unblessed} }; |
73 |
$self->{iteminfo}->{overdue} = $self->{checkout}->is_overdue; |
74 |
|
75 |
if ($self->{item}->itemlost) { |
76 |
$self->{messages}->{WasLost} = 1; |
77 |
if (Koha::RefundLostItemFeeRules->should_refund({ |
78 |
current_branch => $self->{library}->branchcode, |
79 |
item_home_branch => $self->{item}->homebranch, |
80 |
item_holding_branch => $self->{item}->holdingbranch, |
81 |
})) { |
82 |
$self->fixAccountForLostAndReturned(); |
83 |
$self->{messages}->{Itemlost} = $self->{item}->barcode; |
84 |
$self->{messages}->{LostItemFeeRefunded} = 1; |
85 |
} |
86 |
} |
87 |
$self->fixOverduesOnReturn(); |
88 |
} else { # NOT CHECKED OUT, JUST FIX STATUSES, TRANSFERS AND RETURN |
89 |
$self->{error} = Core::Exception::Circulation::CheckinError->new(error => "Unable to checkin\n"); |
90 |
if ($self->{item}->withdrawn) { |
91 |
$self->{messages}->{Withdrawn} = $self->{item}->barcode; |
92 |
} else { |
93 |
$self->{messages}->{NotIssued} = $self->{item}->barcode; |
94 |
} |
95 |
} |
96 |
|
97 |
# TRANSFER |
98 |
my $transfer = $self->{item}->get_transfer; |
99 |
if ($transfer) { |
100 |
if ($transfer->tobranch eq $self->{library}->branchcode) { |
101 |
# ARRIVED AT RIGHT BRANCH |
102 |
$transfer->datearrived(_today())->store; |
103 |
} else { |
104 |
# WRONG BRANCH - Should we fix transfers at once? |
105 |
$self->{messages}->{WrongTransfer} = $transfer->tobranch; |
106 |
$self->{messages}->{WrongTransferItem} = $self->{item}->itemnumber; |
107 |
} |
108 |
} |
109 |
|
110 |
# HOLD |
111 |
my $hold = $self->getPendingHold; |
112 |
if ($hold) { |
113 |
if ($hold->branchcode ne $self->{library}->branchcode) { |
114 |
$self->{messages}->{NeedsTransfer} = $hold->branchcode; |
115 |
} |
116 |
# Weird message return yes, but this is how Koha templates expect it |
117 |
$self->{messages}->{ResFound} = { |
118 |
%{$hold->unblessed}, |
119 |
ResFound => sub {$hold->is_waiting ? return "Waiting" : return "Reserved"}, |
120 |
}; |
121 |
} |
122 |
|
123 |
# WRONG BRANCH - CREATE TRANSFER IF NOT ONE ALREADY |
124 |
if ($self->{library}->branchcode ne $self->{item}->homebranch and not $transfer) { |
125 |
Koha::Item::Transfer->new({ |
126 |
itemnumber => $self->{item}->itemnumber, |
127 |
frombranch => $self->{library}->branchcode, |
128 |
tobranch => $self->{item}->homebranch, |
129 |
datesent => _today(), |
130 |
})->store; |
131 |
$self->{messages}->{WasTransfered} = 1; |
132 |
# IF AutomaticItemReturn false - set NeedsTransfer |
133 |
$self->{prefs}->{AutomaticItemReturn} and $self->{messages}->{NeedsTransfer} = $self->{item}->homebranch; |
134 |
} |
135 |
|
136 |
# TODO: Rewrite to Core? |
137 |
C4::Stats::UpdateStats({ |
138 |
branch => $self->{library}->branchcode, |
139 |
type => "return", |
140 |
itemnumber => $self->{item}->itemnumber, |
141 |
itemtype => $self->{item}->itype, |
142 |
borrowernumber => $self->{patron} ? $self->{patron}->borrowernumber : undef, |
143 |
ccode => $self->{item}->ccode, |
144 |
}); |
145 |
|
146 |
# TODO: Borrower message (SendCirculationAlert) or HANDLE IN PrintSlip ? |
147 |
$self->sendCirculationAlert(); |
148 |
|
149 |
$self->removeOverdueDebarments(); |
150 |
|
151 |
# RETURN |
152 |
if ($self->{checkout}) { |
153 |
$self->{checkout}->returndate($returnDate)->store; |
154 |
if ($self->{checkout}->patron->privacy == 2) { |
155 |
$self->{checkout}->borrowernumber(undef)->store; |
156 |
} else { |
157 |
$self->{patron} and $self->{item}->last_returned_by( $self->{patron} ); |
158 |
} |
159 |
# MERGE TABLES - PLEASE |
160 |
my $old_checkout = Koha::Old::Checkout->new($self->{checkout}->unblessed)->store; |
161 |
$self->{checkout}->delete; |
162 |
# TODO: Needs to decide if $self->{checkout} should be emptied upon checkout or not |
163 |
# As templates require checkout object we must leave it for now |
164 |
# undef $self->{checkout}; |
165 |
$self->{messages}->{WasReturned} = 1; |
166 |
} else { |
167 |
$self->{error} = Core::Exception::Circulation::CheckinError->new(error => "Unable to checkin\n"); |
168 |
$self->{messages}->{WasReturned} = 0; |
169 |
$self->{messages}->{DataCorrupted} = 1; |
170 |
} |
171 |
|
172 |
# TODO: rewrite all methods to overridable circulation methods |
173 |
# Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer |
174 |
my $returnbranch = $self->getBranchItemRule(); |
175 |
my $is_in_rotating_collection = C4::RotatingCollections::isItemInAnyCollection( $self->{item}->itemnumber ); |
176 |
|
177 |
if (!$is_in_rotating_collection && $self->{messages}->{NotIssued} and !$hold and ($self->{item}->homebranch ne $returnbranch) and not $self->{messages}->{WrongTransfer}) { |
178 |
if ( $self->{prefs}->{AutomaticItemReturn} or ( $self->{prefs}->{UseBranchTransferLimits} |
179 |
and ! C4::Circulation::IsBranchTransferAllowed($self->{item}->homebranch, $returnbranch, $self->{item}->{$self->{prefs}->{BranchTransferLimitsType}} ) |
180 |
)) { |
181 |
# AutomaticItemReturn: transfer to returnbranch |
182 |
$transfer and $transfer->set({ |
183 |
datearrived => \"NOW()", |
184 |
comments => undef, |
185 |
}); |
186 |
Koha::Item::Transfer->new({ |
187 |
itemnumber => $self->{item}->itemnumber, |
188 |
frombranch => $self->{library}->branchcode, |
189 |
tobranch => $returnbranch, |
190 |
datesent => _today(), |
191 |
})->store; |
192 |
$self->{prefs}->{ReturnToShelvingCart} and $self->cartToShelf(); |
193 |
$self->{messages}->{WasTransfered} = 1; |
194 |
} else { |
195 |
$self->{messages}->{NeedsTransfer} = $returnbranch; |
196 |
} |
197 |
} |
198 |
|
199 |
# UPDATE ITEM |
200 |
$self->{item}->set({ |
201 |
holdingbranch => $returnbranch ? $returnbranch : $self->{library}->branchcode, |
202 |
onloan => undef, |
203 |
datelastseen => _today(), |
204 |
itemlost => 0, |
205 |
})->store; |
206 |
|
207 |
my $dt = Time::HiRes::time() - $t0; |
208 |
printf STDERR "[%s] Checkin called : %s - elapsed %.3fs\n", ref $self, scalar(gmtime()), $dt; |
209 |
return $self; |
210 |
} |
211 |
|
212 |
# IN -> $self, $dateDue[, $cancelReserve, $issueDate, $sipMode] |
213 |
sub Checkout { |
214 |
my ($self, $dateDue, $cancelReserve, $issueDate, $sipMode, $params) = @_; # Surely most params can be skipped here |
215 |
$self->{patron} and $self->{library} or Core::Exception::Circulation::CheckinError->throw(error => "No checkout without patron and/or library!\n"); |
216 |
|
217 |
# extra params |
218 |
my $onsite_checkout = $params && $params->{onsite_checkout} ? 1 : 0; |
219 |
my $switch_onsite_checkout = $params && $params->{switch_onsite_checkout}; |
220 |
my $auto_renew = $params && $params->{auto_renew}; |
221 |
|
222 |
my $t0 = Time::HiRes::time(); |
223 |
warn "Checking out item " . $self->{item}->barcode . " for patron " . $self->{patron}->borrowernumber . "\n"; |
224 |
$issueDate ||= _today(); |
225 |
|
226 |
# PREFS THAT APPLY |
227 |
# AllowReturnToBranch |
228 |
my $circControlBranch = $self->getCircControlBranch(); |
229 |
|
230 |
# ALREADY CHECKED OUT? |
231 |
if ($self->{checkout}) { |
232 |
if ($self->{checkout}->patron->borrowernumber eq $self->{patron}->borrowernumber) { |
233 |
if ($self->canItemBeRenewed() and not $switch_onsite_checkout) { |
234 |
$self->Renew( $dateDue ); |
235 |
} |
236 |
} else { |
237 |
# Wrong checkout, return first? |
238 |
if ($self->canItemBeReturned() and not $switch_onsite_checkout) { |
239 |
$self->Checkin(); |
240 |
} |
241 |
} |
242 |
} else { |
243 |
|
244 |
# TRANSFERS AND HOLDS |
245 |
my $transfer = $self->{item}->get_transfer; |
246 |
if ($transfer) { |
247 |
if ($transfer->tobranch eq $self->{library}->branchcode) { |
248 |
$transfer->datearrived(_today())->store; # Finish transfer |
249 |
} else { |
250 |
# Update transfer, or should we close and create new? |
251 |
$transfer->frombranch($self->{library}->branchcode)->store; |
252 |
} |
253 |
} |
254 |
|
255 |
# TODO: C4::Circulation.pm:1317 |
256 |
# MoveReserve( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $cancelreserve ); |
257 |
# -> C4::Reserves.pm:1712 |
258 |
# -> CheckReserves C4::Reserves.pm:626 |
259 |
|
260 |
# HOLD |
261 |
my $hold = $self->getPendingHold(); |
262 |
if ($hold) { |
263 |
# Right patron - fill hold |
264 |
if ($hold->borrowernumber == $self->{patron}->borrowernumber) { |
265 |
$hold->set({found => "F", priority => 0}); |
266 |
# Merge tables - PLEASE! |
267 |
Koha::Old::Hold->new( $hold->unblessed() )->store(); |
268 |
$hold->delete(); |
269 |
} else { |
270 |
# TODO: Checkout should be aborted here? |
271 |
$self->{error} = Core::Exception::Circulation::CheckoutError->new("Unable to checkout - hold for another patron!"); |
272 |
} |
273 |
} |
274 |
|
275 |
# CIRCULATION RULES |
276 |
my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule({ |
277 |
categorycode => $self->{patron}->{categorycode}, |
278 |
itemtype => $self->{item}->itype, |
279 |
branchcode => $self->{library}->branchcode, |
280 |
}); |
281 |
|
282 |
# CHECKOUT |
283 |
# TODO: rewrite calculate dateDue to overridable module |
284 |
unless ($dateDue) { |
285 |
my $itype = $self->{item}->effective_itemtype; |
286 |
$dateDue = C4::Circulation::CalcDateDue( $issueDate, $itype, $self->{library}->branchcode, $self->{patron}->unblessed, 1 ); |
287 |
|
288 |
} |
289 |
$dateDue->truncate( to => "minute" ); |
290 |
|
291 |
# TODO: auto_renew and onsite_checkout |
292 |
$self->{checkout} = Koha::Checkout->new({ |
293 |
borrowernumber => $self->{patron}->borrowernumber, |
294 |
itemnumber => $self->{item}->itemnumber, |
295 |
issuedate => $issueDate, |
296 |
date_due => $dateDue, |
297 |
branchcode => $self->{library}->branchcode, |
298 |
})->store; |
299 |
|
300 |
# Why was totalissues never set? |
301 |
my $totalissues = $self->{item}->biblioitem->totalissues // 0; |
302 |
$self->{item}->biblioitem->totalissues($totalissues + 1); |
303 |
|
304 |
# LOST ITEM REFUND |
305 |
if ($self->{item}->itemlost) { |
306 |
# REFUND LOST ITEM FEE |
307 |
if (Koha::RefundLostItemFeeRules->should_refund({ |
308 |
current_branch => $self->{library}->branchcode, |
309 |
item_home_branch => $self->{item}->homebranch, |
310 |
item_holding_branch => $self->{item}->holdingbranch, |
311 |
})) { |
312 |
$self->fixAccountForLostAndReturned(); |
313 |
} |
314 |
} |
315 |
|
316 |
# UPDATE ITEM |
317 |
$self->{item}->set({ |
318 |
issues => $self->{item}->issues + 1, |
319 |
holdingbranch => $self->{library}->branchcode, |
320 |
itemlost => 0, |
321 |
onloan => $dateDue->ymd(), |
322 |
datelastborrowed => _today(), |
323 |
datelastseen => _today(), |
324 |
})->store; |
325 |
|
326 |
C4::Stats::UpdateStats({ |
327 |
branch => $self->{library}->branchcode, |
328 |
type => "issue", |
329 |
other => ( $sipMode ? "SIP-$sipMode" : "" ), |
330 |
itemnumber => $self->{item}->itemnumber, |
331 |
itemtype => $self->{item}->itype, |
332 |
location => $self->{item}->location, |
333 |
borrowernumber => $self->{patron}->borrowernumber, |
334 |
ccode => $self->{item}->ccode, |
335 |
}); |
336 |
|
337 |
# TODO: Borrower message (SendCirculationAlert) HANDLE IN PrintSlip ? |
338 |
$self->sendCirculationAlert(); |
339 |
|
340 |
$self->removeOverdueDebarments(); |
341 |
} |
342 |
my $dt = Time::HiRes::time() - $t0; |
343 |
printf STDERR "[%s] Checkout called : %s - elapsed %.3fs\n", ref $self, scalar(gmtime()), $dt; |
344 |
return $self; |
345 |
|
346 |
} |
347 |
|
348 |
sub Renew { |
349 |
my ($self, $dateDue, $lastRenewedDate) = @_; # Probably can skip most of these |
350 |
$self->{patron} and $self->{library} or die "Do we really need patron and library?"; |
351 |
warn "Renewing item " . $self->{item}->itemnumber . " for patron " . $self->{patron}->borrowernumber . "\n"; |
352 |
my $t0 = Time::HiRes::time(); |
353 |
|
354 |
$self->{checkout} or die "No checkout, cannot renew"; |
355 |
$lastRenewedDate ||= _today(); |
356 |
|
357 |
# PREFS THAT APPLY |
358 |
# CalculateFinesOnReturn |
359 |
# RenewalPeriodBase |
360 |
# RenewalSendNotice |
361 |
# CircControl |
362 |
|
363 |
$self->fixOverduesOnReturn(); |
364 |
$self->canItemBeRenewed() or return $self; |
365 |
|
366 |
# default to checkout branch |
367 |
my $circControlBranch = $self->getCircControlBranch(); |
368 |
|
369 |
# If the due date wasn't specified, calculate it by adding the |
370 |
# book's loan length to today's date or the current due date |
371 |
# based on the value of the RenewalPeriodBase syspref. |
372 |
if ( defined $dateDue && ref $dateDue ne "DateTime" ) { |
373 |
carp "Invalid date passed to Renew."; |
374 |
return; |
375 |
} |
376 |
# TODO: Adjust to use calendar |
377 |
unless ($dateDue) { |
378 |
my $itemType = $self->{item}->effective_itemtype; |
379 |
if ($self->{prefs}->{RenewalPeriodBase} eq "date_due") { |
380 |
$dateDue = _mysqldate2dt($self->{checkout}->date_due); |
381 |
} else { |
382 |
$dateDue = _today(); |
383 |
} |
384 |
$dateDue = C4::Circulation::CalcDateDue($dateDue, $itemType, $circControlBranch, $self->{patron}->unblessed, 1); |
385 |
} |
386 |
|
387 |
# UPDATE CHECKOUT AND ITEM |
388 |
my $checkoutRenewals = $self->{checkout}->renewals || 0; |
389 |
my $itemRenewals = $self->{item}->renewals || 0; |
390 |
$self->{checkout}->set({ |
391 |
date_due => _dt2mysqldate($dateDue), |
392 |
lastreneweddate => $lastRenewedDate, |
393 |
renewals => $checkoutRenewals + 1, |
394 |
})->store; |
395 |
|
396 |
$self->{item}->set({ |
397 |
renewals => $itemRenewals + 1, |
398 |
onloan => _dt2mysqldate($dateDue), |
399 |
})->store; |
400 |
|
401 |
# TODO: Renewal notice? |
402 |
$self->removeOverdueDebarments(); |
403 |
|
404 |
# STATS |
405 |
C4::Circulation::UpdateStats({ |
406 |
branch => $self->{checkout}->branchcode, |
407 |
type => "renew", |
408 |
amount => undef, |
409 |
itemnumber => $self->{item}->itemnumber, |
410 |
itemtype => $self->{item}->itype, |
411 |
location => $self->{item}->location, |
412 |
borrowernumber => $self->{patron}->borrowernumber, |
413 |
ccode => $self->{item}->ccode, |
414 |
}); |
415 |
my $dt = Time::HiRes::time() - $t0; |
416 |
printf STDERR "[%s] Renew called : %s - elapsed %.3fs\n", ref $self, scalar(gmtime()), $dt; |
417 |
return $self; |
418 |
} |
419 |
|
420 |
=head |
421 |
getBranchItemRule |
422 |
use legacy C4::Circulation::GetBranchItemRule |
423 |
=cut |
424 |
sub getBranchItemRule { |
425 |
my $self = shift; |
426 |
use C4::Circulation; |
427 |
my $hbr = C4::Circulation::GetBranchItemRule($self->{item}->homebranch, $self->{item}->itype)->{'returnbranch'} || "homebranch"; |
428 |
return $self->{item}->{$hbr} || $self->{item}->homebranch ; |
429 |
} |
430 |
|
431 |
=head2 $self->cartToShelf |
432 |
|
433 |
Set the current shelving location of the item record |
434 |
to its stored permanent shelving location. This is |
435 |
primarily used to indicate when an item whose current |
436 |
location is a special processing ('PROC') or shelving cart |
437 |
('CART') location is back in the stacks. |
438 |
|
439 |
=cut |
440 |
|
441 |
sub cartToShelf { |
442 |
my $self = shift; |
443 |
if ($self->{item}->location eq "CART") { |
444 |
$self->{item}->set({ location => $self->{item}->permanent_location })->store; |
445 |
} |
446 |
return $self; |
447 |
} |
448 |
|
449 |
=head2 $self->getCircControlBranch |
450 |
|
451 |
Return the library code to be used to determine which circulation |
452 |
policy applies to a transaction. Looks up the CircControl and |
453 |
HomeOrHoldingBranch system preferences. |
454 |
|
455 |
=cut |
456 |
|
457 |
sub getCircControlBranch { |
458 |
my $self = shift; |
459 |
my $branch; |
460 |
|
461 |
if ($self->{prefs}->{CircControl} eq "PickupLibrary" and $self->{checkout}) { |
462 |
$branch = $self->{checkout}->branchcode; # Why C4::Context->userenv->{'branch'} in C4::Circulation? |
463 |
} elsif ($self->{prefs}->{CircControl} eq "PatronLibrary") { |
464 |
$branch = $self->{patron}->branchcode; |
465 |
} else { |
466 |
my $branchfield = $self->{prefs}->{HomeOrHoldingBranch} || "homebranch"; |
467 |
$branch = $self->{item}->{$branchfield} || $self->{item}->{homebranch}; |
468 |
} |
469 |
return $branch; |
470 |
} |
471 |
|
472 |
=head $self->fixAccountForLostAndReturned |
473 |
If item lost, refund and mark as lost returned |
474 |
=cut |
475 |
sub fixAccountForLostAndReturned { |
476 |
my $self = shift; |
477 |
my $acc = Koha::Account::Lines->search( |
478 |
{ |
479 |
itemnumber => $self->{item}->itemnumber, |
480 |
accounttype => { -in => [ "L", "Rep" ] }, |
481 |
}, |
482 |
{ |
483 |
order_by => { -desc => [ "date", "accountno" ] } |
484 |
} |
485 |
)->next(); |
486 |
return unless $acc; |
487 |
|
488 |
$acc->accounttype("LR"); |
489 |
$acc->store(); |
490 |
|
491 |
my $account = Koha::Account->new( { patron_id => $acc->borrowernumber } ); |
492 |
my $credit_id = $account->pay( |
493 |
{ |
494 |
amount => $acc->amount, |
495 |
description => "Item Returned " . $self->{item}->barcode, |
496 |
account_type => "CR", |
497 |
offset_type => "Lost Item Return", |
498 |
accounlines => [$acc], |
499 |
|
500 |
} |
501 |
); |
502 |
|
503 |
return $credit_id; |
504 |
} |
505 |
|
506 |
=head |
507 |
ref C4::Circulation.pm L2369 |
508 |
From what I could decipher, this sets fine accounttype to F in normal cases, rest is handled in cronjobs |
509 |
TODO: |
510 |
if params exemptfine -> add line with credit and type "Forgiven" |
511 |
if params dropbox -> add line with credit and type "Dropbox" |
512 |
|
513 |
=cut |
514 |
sub fixOverduesOnReturn { |
515 |
my $self = shift; |
516 |
|
517 |
$self->{patron} and $self->{item} or croak "Missing patron or item"; |
518 |
|
519 |
my $accountline = Koha::Account::Lines->search( |
520 |
{ |
521 |
borrowernumber => $self->{patron}->borrowernumber, |
522 |
itemnumber => $self->{item}->itemnumber, |
523 |
-or => [ |
524 |
accounttype => "FU", |
525 |
accounttype => "O", |
526 |
], |
527 |
} |
528 |
)->next(); |
529 |
$accountline and $accountline->accounttype("F")->store; |
530 |
return $self; |
531 |
} |
532 |
|
533 |
=head |
534 |
ref C4::Circulation L676 |
535 |
|
536 |
=cut |
537 |
sub canItemBeIssued { |
538 |
my ($self, $dateDue, $ignoreReserves) = @_; |
539 |
# my ( $borrower, $barcode, $duedate, $inprocess, $ignore_reserves, $params ) = @_; |
540 |
my $needsconfirmation; # filled with problems that needs confirmations |
541 |
my $issuingimpossible; # filled with problems that causes the issue to be IMPOSSIBLE |
542 |
my $alerts; # filled with messages that shouldn't stop issuing, but the librarian should be aware of. |
543 |
my $messages; # filled with information messages-> that should be displayed. |
544 |
|
545 |
# my $onsite_checkout = $params->{onsite_checkout} || 0; |
546 |
# my $override_high_holds = $params->{override_high_holds} || 0; |
547 |
|
548 |
# PREFS |
549 |
# noissuescharge |
550 |
# AllowFineOverride |
551 |
# AllFinesNeedOverride |
552 |
# NoIssuesChargeGuarantees |
553 |
# This should be returned before calling Core::Circulation |
554 |
$self->{item}->itemnumber or return ($self, {UNKNOWN_BARCODE => 1}); |
555 |
# TODO: should dateDue be automatically calculated? |
556 |
# Makes no sense to ask for dateDue |
557 |
# unless ($dateDue) { |
558 |
# my $itype = $self->{item}->effective_itemtype; |
559 |
# $dateDue = C4::Circulation::CalcDateDue( _today(), $itype, $self->{library}->branchcode, $self->{patron}->unblessed, 1 ); |
560 |
|
561 |
# } |
562 |
# $dateDue->truncate( to => "minute" ); |
563 |
|
564 |
# PATRON FLAGS |
565 |
$self->{patron}->gonenoaddress and $issuingimpossible->{GNA} = 1; |
566 |
$self->{patron}->lost and $issuingimpossible->{CARD_LOST} = 1; |
567 |
$self->{patron}->debarred and $issuingimpossible->{DEBARRED} = 1; |
568 |
$self->{patron}->is_expired and $issuingimpossible->{EXPIRED} = 1; |
569 |
|
570 |
# DEBTS |
571 |
# Note: Koha::Account is different than other Koha Objects |
572 |
my $debt = Koha::Account->new({patron_id => $self->{patron}->borrowernumber})->balance; |
573 |
if ($debt > 0) { |
574 |
# TODO: if $self->{prefs}->{IssuingInProcess} |
575 |
if ($debt > $self->{prefs}->{noissuescharge} and not $self->{prefs}->{AllowFineOverride}) { |
576 |
$issuingimpossible->{DEBT} = sprintf( "%.2f", $debt ); |
577 |
} |
578 |
} elsif ($self->{prefs}->{NoIssuesChargeGuarantees}) { |
579 |
my $guarantees_charges; |
580 |
foreach my $g ( @$self->{patron}->guarantees ) { |
581 |
$guarantees_charges += Koha::Account->new({patron_id => $g->borrowernumber})->balance; |
582 |
} |
583 |
if ( $guarantees_charges > $self->{prefs}->{NoIssuesChargeGuarantees} and not $self->{prefs}->{AllowFineOverride}) { |
584 |
$issuingimpossible->{DEBT_GUARANTEES} = sprintf( "%.2f", $$guarantees_charges ); |
585 |
} |
586 |
} |
587 |
|
588 |
# DEBARRED |
589 |
if ( my $debarred_date = $self->{patron}->is_debarred ) { |
590 |
if ($debarred_date eq "9999-12-31") { |
591 |
$issuingimpossible->{USERBLOCKEDNOENDDATE} = $debarred_date; |
592 |
} else { |
593 |
$issuingimpossible->{USERBLOCKEDWITHENDDATE} = $debarred_date; |
594 |
} |
595 |
} elsif ( my $num_overdues = $self->{patron}->has_overdues ) { |
596 |
if ( C4::Context->preference("OverduesBlockCirc") eq "block") { |
597 |
$issuingimpossible->{USERBLOCKEDOVERDUE} = $num_overdues; |
598 |
} |
599 |
elsif ( C4::Context->preference("OverduesBlockCirc") eq "confirmation") { |
600 |
$needsconfirmation->{USERBLOCKEDOVERDUE} = $num_overdues; |
601 |
} |
602 |
} |
603 |
|
604 |
# ALREADY CHECKED OUT |
605 |
if ($self->{checkout}) { |
606 |
# SAME PATRON |
607 |
if ($self->{checkout}->borrowernumber eq $self->{patron}->borrowernumber ) { |
608 |
if ( $self->{checkout}->onsite_checkout and $self->{prefs}->{SwitchOnSiteCheckouts}) { |
609 |
$messages->{ONSITE_CHECKOUT_WILL_BE_SWITCHED} = 1; |
610 |
} else { |
611 |
my ($ok, $error) = $self->canItemBeRenewed(); |
612 |
if ($ok) { |
613 |
if ( $error eq "onsite_checkout" ) { |
614 |
$issuingimpossible->{NO_RENEWAL_FOR_ONSITE_CHECKOUTS} = 1; |
615 |
} else { |
616 |
$issuingimpossible->{NO_MORE_RENEWALS} = 1; |
617 |
} |
618 |
} else { |
619 |
$needsconfirmation->{RENEW_ISSUE} = 1; |
620 |
} |
621 |
} |
622 |
# DIFFERENT PATRON |
623 |
} else { |
624 |
my ( $ok, $message ) = $self->canItemBeReturned(); |
625 |
if ( $ok ) { |
626 |
$needsconfirmation->{ISSUED_TO_ANOTHER} = 1; |
627 |
# THESE ARE JUST SILLY - should be removed |
628 |
$needsconfirmation->{issued_firstname} = $self->{checkout}->patron->firstname; |
629 |
$needsconfirmation->{issued_surname} = $self->{checkout}->patron->surname; |
630 |
$needsconfirmation->{issued_cardnumber} = $self->{checkout}->patron->cardnumber; |
631 |
$needsconfirmation->{issued_borrowernumber} = $self->{checkout}->patron->borrowernumber; |
632 |
} else { |
633 |
$issuingimpossible->{RETURN_IMPOSSIBLE} = 1; |
634 |
$issuingimpossible->{branch_to_return} = $message; |
635 |
} |
636 |
} |
637 |
} |
638 |
|
639 |
# TODO: Too Many checkouts |
640 |
# JB34 CHECKS IF BORROWERS DON'T HAVE ISSUE TOO MANY BOOKS |
641 |
# |
642 |
# my $switch_onsite_checkout = ( |
643 |
# C4::Context->preference('SwitchOnSiteCheckouts') |
644 |
# and $issue |
645 |
# and $issue->onsite_checkout |
646 |
# and $issue->borrowernumber == $borrower->{'borrowernumber'} ? 1 : 0 ); |
647 |
# my $toomany = TooMany( $borrower, $item->{biblionumber}, $item, { onsite_checkout => $onsite_checkout, switch_onsite_checkout => $switch_onsite_checkout, } ); |
648 |
# # if TooMany max_allowed returns 0 the user doesn't have permission to check out this book |
649 |
# if ( $toomany && not exists $needsconfirmation->{RENEW_ISSUE} ) { |
650 |
# if ( $toomany->{max_allowed} == 0 ) { |
651 |
# $needsconfirmation->{PATRON_CANT} = 1; |
652 |
# } |
653 |
# if ( C4::Context->preference("AllowTooManyOverride") ) { |
654 |
# $needsconfirmation->{TOO_MANY} = $toomany->{reason}; |
655 |
# $needsconfirmation->{current_loan_count} = $toomany->{count}; |
656 |
# $needsconfirmation->{max_loans_allowed} = $toomany->{max_allowed}; |
657 |
# } else { |
658 |
# $issuingimpossible->{TOO_MANY} = $toomany->{reason}; |
659 |
# $issuingimpossible->{current_loan_count} = $toomany->{count}; |
660 |
# $issuingimpossible->{max_loans_allowed} = $toomany->{max_allowed}; |
661 |
# } |
662 |
# } |
663 |
|
664 |
# CHECKPREVCHECKOUT: CHECK IF ITEM HAS EVER BEEN LENT TO PATRON |
665 |
if ($self->{patron}->wants_check_for_previous_checkout and $self->{patron}->do_check_for_previous_checkout($self->{item})) { |
666 |
$needsconfirmation->{PREVISSUE} = 1 |
667 |
} |
668 |
|
669 |
# ITEM BASED RESTRICTIONS |
670 |
# TODO: |
671 |
# Age restriction |
672 |
# pref item-level_itypes |
673 |
# independentBranches |
674 |
# rental charges |
675 |
# decreaseLoanHighHolds |
676 |
|
677 |
$self->{item}->withdrawn > 0 and $issuingimpossible->{WTHDRAWN} = 1; |
678 |
if ($self->{item}->restricted and $self->{item}->restricted > 0) { |
679 |
$issuingimpossible->{RESTRICTED} = 1; |
680 |
} |
681 |
|
682 |
if ($self->{item}->notforloan) { |
683 |
if ($self->{prefs}->{AllowNotForLoanOverride}) { |
684 |
$issuingimpossible->{NOT_FOR_LOAN} = 1; |
685 |
$issuingimpossible->{item_notforloan} = $self->{item}->notforloan; # WHY THIS? |
686 |
} else { |
687 |
$needsconfirmation->{NOT_FOR_LOAN_FORCING} = 1; |
688 |
$needsconfirmation->{item_notforloan} = $self->{item}->notforloan; # WHY THIS? |
689 |
} |
690 |
} |
691 |
|
692 |
if ( $self->{item}->itemlost and $self->{prefs}->{IssueLostItem} ne "nothing" ) { |
693 |
my $av = Koha::AuthorisedValues->search({ category => 'LOST', authorised_value => $self->{item}->itemlost }); |
694 |
my $code = $av->count ? $av->next->lib : ""; |
695 |
( $self->{prefs}->{IssueLostItem} eq "confirm" ) and $needsconfirmation->{ITEM_LOST} = $code; |
696 |
( $self->{prefs}->{IssueLostItem} eq "alert" ) and $alerts->{ITEM_LOST} = $code; # TODO: alert only used once, should be removed |
697 |
} |
698 |
|
699 |
unless ( $ignoreReserves ) { |
700 |
my $hold = $self->getPendingHold(); |
701 |
if ($hold) { |
702 |
if ($hold->is_waiting) { |
703 |
$needsconfirmation->{RESERVE_WAITING} = 1; |
704 |
$needsconfirmation->{'reswaitingdate'} = $hold->waitingdate; |
705 |
} else { |
706 |
$needsconfirmation->{RESERVED} = 1; |
707 |
} |
708 |
|
709 |
$needsconfirmation->{'resfirstname'} = $hold->patron->firstname; |
710 |
$needsconfirmation->{'ressurname'} = $hold->patron->surname; |
711 |
$needsconfirmation->{'rescardnumber'} = $hold->patron->cardnumber; |
712 |
$needsconfirmation->{'resborrowernumber'} = $hold->patron->borrowernumber; |
713 |
$needsconfirmation->{'resbranchcode'} = $hold->patron->branchcode; |
714 |
$needsconfirmation->{'resreservedate'} = $hold->reservedate; |
715 |
} |
716 |
} |
717 |
|
718 |
# TODO: consider if this is neccessary |
719 |
if ( not $self->{prefs}->{AllowMultipleIssuesOnABiblio} and not $issuingimpossible->{NO_MORE_RENEWALS} and not $needsconfirmation->{RENEW_ISSUE}) { |
720 |
# don't do the multiple loans per bib check if we've |
721 |
# already determined that we've got a loan on the same item |
722 |
unless ($self->{item}->biblio->subscriptions->count) { |
723 |
my $checkouts = Koha::Checkouts->search({ |
724 |
borrowernumber => $self->{patron}->borrowernumber, |
725 |
biblionumber => $self->{item}->biblionumber, |
726 |
}, { |
727 |
join => 'item', |
728 |
}); |
729 |
if ( $checkouts->count ) { |
730 |
$needsconfirmation->{BIBLIO_ALREADY_ISSUED} = 1; |
731 |
} |
732 |
} |
733 |
} |
734 |
|
735 |
return ( $issuingimpossible, $needsconfirmation, $alerts, $messages ); |
736 |
} |
737 |
|
738 |
=head |
739 |
$self->canItemBeRenewed |
740 |
if no holds binds item, allow renewal |
741 |
args: |
742 |
override => bool |
743 |
return: |
744 |
bool => can/cannot be renewed |
745 |
msg => legacy error msg string |
746 |
=cut |
747 |
|
748 |
sub canItemBeRenewed { |
749 |
my ($self, $override) = @_; |
750 |
$self->{checkout} or return ( 0, "no_checkout" ); |
751 |
|
752 |
# PREFS THAT MIGHT APPLY |
753 |
# OverduesBlockRenewing |
754 |
# RestrictionBlockRenewing |
755 |
|
756 |
# Too few items to fill holds means item cannot be renewed |
757 |
if (not $self->canItemFillHold and not $override) { |
758 |
return ( 0, "on_hold" ); |
759 |
} |
760 |
|
761 |
# CIRCULATION RULES |
762 |
my $circ_rule = Koha::IssuingRules->get_effective_issuing_rule( |
763 |
{ categorycode => $self->{patron}->categorycode, |
764 |
itemtype => $self->{item}->itype, |
765 |
branchcode => $self->{checkout}->branchcode, |
766 |
} |
767 |
); |
768 |
|
769 |
my $renewals = $self->{checkout}->renewals || 0; |
770 |
$circ_rule or return ( 0, "too_many" ); # MISSING CIRC RULE - too_many in original, but should perhaps be better worded |
771 |
$circ_rule->renewalsallowed <= $renewals or return ( 0, "too_many" ); |
772 |
|
773 |
|
774 |
$self->{patron}->is_debarred and return ( 0, "restriction" ); |
775 |
($self->{patron}->has_overdues or $self->{checkout}->is_overdue) and return ( 0, "overdue" ); |
776 |
|
777 |
# TODO: AUTORENEW - USE CASE? |
778 |
|
779 |
# NO RENEWAL BEFORE |
780 |
if ( defined $circ_rule->norenewalbefore and $circ_rule->norenewalbefore ne "" ) { |
781 |
my $dt = _mysqldate2dt($self->{checkout}->date_due); |
782 |
my $soonestrenewal = $dt->subtract( $circ_rule->lengthunit => $circ_rule->norenewalbefore ); |
783 |
if ( $soonestrenewal > DateTime->now() ) { |
784 |
return ( 0, "too_soon" ); |
785 |
} |
786 |
} |
787 |
return (1, undef); |
788 |
} |
789 |
|
790 |
sub canItemBeReturned { |
791 |
my $self = shift; |
792 |
my $allowreturntobranch = $self->{prefs}->{AllowReturnToBranch}; |
793 |
|
794 |
# assume return is allowed to start |
795 |
my $allowed = 1; |
796 |
my $message; |
797 |
|
798 |
# identify all cases where return is forbidden |
799 |
if ($allowreturntobranch eq "homebranch" && $self->{library}->branchcode ne $self->{item}->homebranch) { |
800 |
$allowed = 0; |
801 |
$message = $self->{item}->homebranch; |
802 |
} elsif ($allowreturntobranch eq "holdingbranch" && $self->{library}->branchcode ne $self->{item}->holdingbranch) { |
803 |
$allowed = 0; |
804 |
$message = $self->{item}->holdingbranch; |
805 |
} elsif ($allowreturntobranch eq "homeorholdingbranch" && $self->{library}->branchcode ne $self->{item}->homebranch |
806 |
&& $self->{library}->branchcode ne $self->{item}->holdingbranch) { |
807 |
$allowed = 0; |
808 |
$message = $self->{item}->homebranch; # FIXME: choice of homebranch is arbitrary |
809 |
} |
810 |
|
811 |
return ($allowed, $message); |
812 |
} |
813 |
|
814 |
=head |
815 |
Check if item can fill a pending hold |
816 |
reservable means: |
817 |
- item not found (Waiting or in Transfer) |
818 |
- item not lost, withdrawn |
819 |
- item notforloan not code > 0 |
820 |
- item not damaged (unless AllowHoldsOnDamagedItems) |
821 |
- number of reservable items is greater than number of pending holds |
822 |
- something something onshelfholds |
823 |
|
824 |
ref: C4::Reserves::CanItemBeReserved L282 |
825 |
=cut |
826 |
sub canItemFillHold { |
827 |
my $self = shift; |
828 |
|
829 |
# PREFS |
830 |
# AllowRenewalIfOtherItemsAvailable |
831 |
# item-level_itypes |
832 |
# IndependentBranches |
833 |
# canreservefromotherbranches |
834 |
# AllowHoldsOnDamagedItems |
835 |
|
836 |
my $reservableItems = Koha::Items->search({ |
837 |
"me.biblionumber" => $self->{item}->biblionumber, |
838 |
itemlost => 0, |
839 |
withdrawn => 0, |
840 |
notforloan => { "<=" => 0 }, |
841 |
"reserves.found" => { "!=" => undef }, |
842 |
}, |
843 |
{ join => "reserves" }); |
844 |
|
845 |
if ($reservableItems->count < $self->getPendingHoldsByBiblio->count) { |
846 |
return 0; |
847 |
} else { |
848 |
return 1; |
849 |
} |
850 |
} |
851 |
=head |
852 |
return next hold, item or biblio level |
853 |
=cut |
854 |
sub getPendingHold { |
855 |
my $self = shift; |
856 |
my $itemholds = $self->{item}->current_holds; |
857 |
my $biblioholds = Koha::Holds->search({biblionumber => $self->{item}->biblionumber, found => undef, suspend => 0}, { order_by => { -asc => "priority" }}); |
858 |
my $hold = $itemholds->count ? $itemholds->next : $biblioholds->count ? $biblioholds->next : undef; |
859 |
return $hold; |
860 |
} |
861 |
|
862 |
=head |
863 |
return pending holds on biblio level |
864 |
=cut |
865 |
sub getPendingHoldsByBiblio { |
866 |
my $self = shift; |
867 |
return Koha::Holds->search({biblionumber => $self->{item}->biblionumber, found => undef, suspend => 0}, { order_by => { -asc => "priority" }}); |
868 |
} |
869 |
|
870 |
sub _mysqldate2dt { |
871 |
my $datestring = shift; |
872 |
my $strp = DateTime::Format::Strptime->new( pattern => "%Y-%m-%d %H:%M:%S" ); |
873 |
return $strp->parse_datetime( $datestring ); |
874 |
} |
875 |
|
876 |
sub _dt2mysqldate { |
877 |
my $dt = shift; |
878 |
if (ref $dt ne "DateTime") { |
879 |
return $dt; |
880 |
} else { |
881 |
return $dt->strftime("%F %T"); |
882 |
} |
883 |
} |
884 |
|
885 |
sub _ymd2dt { |
886 |
my $ymdstring = shift; |
887 |
my $strp = DateTime::Format::Strptime->new( pattern => "%Y-%m-%d" ); |
888 |
return $strp->parse_datetime( $ymdstring ); |
889 |
} |
890 |
|
891 |
sub _today { |
892 |
return DateTime->now()->ymd; |
893 |
} |
894 |
|
895 |
sub sendCirculationAlert { |
896 |
# TODO |
897 |
} |
898 |
|
899 |
=head |
900 |
Remove any OVERDUES related debarment if the borrower has no overdues |
901 |
=cut |
902 |
sub removeOverdueDebarments { |
903 |
my $self = shift; |
904 |
return unless $self->{patron}; |
905 |
if ( $self->{patron}->is_debarred |
906 |
&& ! $self->{patron}->has_overdues |
907 |
&& @{ Koha::Patron::Debarments::GetDebarments({ borrowernumber => $self->{patron}->borrowernumber, type => "OVERDUES" }) } |
908 |
) { |
909 |
Koha::Patron::Debarments::DelUniqueDebarment({ borrowernumber => $self->{patron}->borrowernumber, type => "OVERDUES" }); |
910 |
} |
911 |
} |
912 |
|
913 |
1; |