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 |
$transfer->tobranch($self->{library}->branchcode)->store; # Update transfer |
251 |
} |
252 |
} |
253 |
|
254 |
# TODO: C4::Circulation.pm:1317 |
255 |
# MoveReserve( $item->{'itemnumber'}, $borrower->{'borrowernumber'}, $cancelreserve ); |
256 |
# -> C4::Reserves.pm:1712 |
257 |
# -> CheckReserves C4::Reserves.pm:626 |
258 |
|
259 |
# HOLD |
260 |
my $hold = $self->getPendingHold(); |
261 |
if ($hold) { |
262 |
# Right patron - fill hold |
263 |
if ($hold->borrowernumber == $self->{patron}->borrowernumber) { |
264 |
$hold->set({found => "F", priority => 0}); |
265 |
# Merge tables - PLEASE! |
266 |
Koha::Old::Hold->new( $hold->unblessed() )->store(); |
267 |
$hold->delete(); |
268 |
} else { |
269 |
# TODO: Checkout should be aborted here? |
270 |
$self->{error} = Core::Exception::Circulation::CheckoutError->new("Unable to checkout - hold for another patron!"); |
271 |
} |
272 |
} |
273 |
|
274 |
# CIRCULATION RULES |
275 |
my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule({ |
276 |
categorycode => $self->{patron}->{categorycode}, |
277 |
itemtype => $self->{item}->itype, |
278 |
branchcode => $self->{library}->branchcode, |
279 |
}); |
280 |
|
281 |
# CHECKOUT |
282 |
# TODO: rewrite calculate dateDue to overridable module |
283 |
unless ($dateDue) { |
284 |
my $itype = $self->{item}->effective_itemtype; |
285 |
$dateDue = C4::Circulation::CalcDateDue( $issueDate, $itype, $self->{library}->branchcode, $self->{patron}->unblessed, 1 ); |
286 |
|
287 |
} |
288 |
$dateDue->truncate( to => "minute" ); |
289 |
|
290 |
# TODO: auto_renew and onsite_checkout |
291 |
$self->{checkout} = Koha::Checkout->new({ |
292 |
borrowernumber => $self->{patron}->borrowernumber, |
293 |
itemnumber => $self->{item}->itemnumber, |
294 |
issuedate => $issueDate, |
295 |
date_due => $dateDue, |
296 |
branchcode => $self->{library}->branchcode, |
297 |
})->store; |
298 |
|
299 |
# Why was totalissues never set? |
300 |
my $totalissues = $self->{item}->biblioitem->totalissues // 0; |
301 |
$self->{item}->biblioitem->totalissues($totalissues + 1); |
302 |
|
303 |
# LOST ITEM REFUND |
304 |
if ($self->{item}->itemlost) { |
305 |
# REFUND LOST ITEM FEE |
306 |
if (Koha::RefundLostItemFeeRules->should_refund({ |
307 |
current_branch => $self->{library}->branchcode, |
308 |
item_home_branch => $self->{item}->homebranch, |
309 |
item_holding_branch => $self->{item}->holdingbranch, |
310 |
})) { |
311 |
$self->fixAccountForLostAndReturned(); |
312 |
} |
313 |
} |
314 |
|
315 |
# UPDATE ITEM |
316 |
$self->{item}->set({ |
317 |
issues => $self->{item}->issues + 1, |
318 |
holdingbranch => $self->{library}->branchcode, |
319 |
itemlost => 0, |
320 |
onloan => $dateDue->ymd(), |
321 |
datelastborrowed => _today(), |
322 |
datelastseen => _today(), |
323 |
})->store; |
324 |
|
325 |
C4::Stats::UpdateStats({ |
326 |
branch => $self->{library}->branchcode, |
327 |
type => "issue", |
328 |
other => ( $sipMode ? "SIP-$sipMode" : "" ), |
329 |
itemnumber => $self->{item}->itemnumber, |
330 |
itemtype => $self->{item}->itype, |
331 |
location => $self->{item}->location, |
332 |
borrowernumber => $self->{patron}->borrowernumber, |
333 |
ccode => $self->{item}->ccode, |
334 |
}); |
335 |
|
336 |
# TODO: Borrower message (SendCirculationAlert) HANDLE IN PrintSlip ? |
337 |
$self->sendCirculationAlert(); |
338 |
|
339 |
$self->removeOverdueDebarments(); |
340 |
} |
341 |
my $dt = Time::HiRes::time() - $t0; |
342 |
printf STDERR "[%s] Checkout called : %s - elapsed %.3fs\n", ref $self, scalar(gmtime()), $dt; |
343 |
return $self; |
344 |
|
345 |
} |
346 |
|
347 |
sub Renew { |
348 |
my ($self, $dateDue, $lastRenewedDate) = @_; # Probably can skip most of these |
349 |
$self->{patron} and $self->{library} or die "Do we really need patron and library?"; |
350 |
warn "Renewing item " . $self->{item}->itemnumber . " for patron " . $self->{patron}->borrowernumber . "\n"; |
351 |
my $t0 = Time::HiRes::time(); |
352 |
|
353 |
$self->{checkout} or die "No checkout, cannot renew"; |
354 |
$lastRenewedDate ||= _today(); |
355 |
|
356 |
# PREFS THAT APPLY |
357 |
# CalculateFinesOnReturn |
358 |
# RenewalPeriodBase |
359 |
# RenewalSendNotice |
360 |
# CircControl |
361 |
|
362 |
$self->fixOverduesOnReturn(); |
363 |
$self->canItemBeRenewed() or return $self; |
364 |
|
365 |
# default to checkout branch |
366 |
my $circControlBranch = $self->getCircControlBranch(); |
367 |
|
368 |
# If the due date wasn't specified, calculate it by adding the |
369 |
# book's loan length to today's date or the current due date |
370 |
# based on the value of the RenewalPeriodBase syspref. |
371 |
if ( defined $dateDue && ref $dateDue ne "DateTime" ) { |
372 |
carp "Invalid date passed to Renew."; |
373 |
return; |
374 |
} |
375 |
# TODO: Adjust to use calendar |
376 |
unless ($dateDue) { |
377 |
my $itemType = $self->{item}->effective_itemtype; |
378 |
if ($self->{prefs}->{RenewalPeriodBase} eq "date_due") { |
379 |
$dateDue = _mysqldate2dt($self->{checkout}->date_due); |
380 |
} else { |
381 |
$dateDue = _today(); |
382 |
} |
383 |
$dateDue = C4::Circulation::CalcDateDue($dateDue, $itemType, $circControlBranch, $self->{patron}->unblessed, 1); |
384 |
} |
385 |
|
386 |
# UPDATE CHECKOUT AND ITEM |
387 |
my $checkoutRenewals = $self->{checkout}->renewals || 0; |
388 |
my $itemRenewals = $self->{item}->renewals || 0; |
389 |
$self->{checkout}->set({ |
390 |
date_due => _dt2mysqldate($dateDue), |
391 |
lastreneweddate => $lastRenewedDate, |
392 |
renewals => $checkoutRenewals + 1, |
393 |
})->store; |
394 |
|
395 |
$self->{item}->set({ |
396 |
renewals => $itemRenewals + 1, |
397 |
onloan => _dt2mysqldate($dateDue), |
398 |
})->store; |
399 |
|
400 |
# TODO: Renewal notice? |
401 |
$self->removeOverdueDebarments(); |
402 |
|
403 |
# STATS |
404 |
C4::Circulation::UpdateStats({ |
405 |
branch => $self->{checkout}->branchcode, |
406 |
type => "renew", |
407 |
amount => undef, |
408 |
itemnumber => $self->{item}->itemnumber, |
409 |
itemtype => $self->{item}->itype, |
410 |
location => $self->{item}->location, |
411 |
borrowernumber => $self->{patron}->borrowernumber, |
412 |
ccode => $self->{item}->ccode, |
413 |
}); |
414 |
my $dt = Time::HiRes::time() - $t0; |
415 |
printf STDERR "[%s] Renew called : %s - elapsed %.3fs\n", ref $self, scalar(gmtime()), $dt; |
416 |
return $self; |
417 |
} |
418 |
|
419 |
=head |
420 |
getBranchItemRule |
421 |
use legacy C4::Circulation::GetBranchItemRule |
422 |
=cut |
423 |
sub getBranchItemRule { |
424 |
my $self = shift; |
425 |
use C4::Circulation; |
426 |
my $hbr = C4::Circulation::GetBranchItemRule($self->{item}->homebranch, $self->{item}->itype)->{'returnbranch'} || "homebranch"; |
427 |
return $self->{item}->{$hbr} || $self->{item}->homebranch ; |
428 |
} |
429 |
|
430 |
=head2 $self->cartToShelf |
431 |
|
432 |
Set the current shelving location of the item record |
433 |
to its stored permanent shelving location. This is |
434 |
primarily used to indicate when an item whose current |
435 |
location is a special processing ('PROC') or shelving cart |
436 |
('CART') location is back in the stacks. |
437 |
|
438 |
=cut |
439 |
|
440 |
sub cartToShelf { |
441 |
my $self = shift; |
442 |
if ($self->{item}->location eq "CART") { |
443 |
$self->{item}->set({ location => $self->{item}->permanent_location })->store; |
444 |
} |
445 |
return $self; |
446 |
} |
447 |
|
448 |
=head2 $self->getCircControlBranch |
449 |
|
450 |
Return the library code to be used to determine which circulation |
451 |
policy applies to a transaction. Looks up the CircControl and |
452 |
HomeOrHoldingBranch system preferences. |
453 |
|
454 |
=cut |
455 |
|
456 |
sub getCircControlBranch { |
457 |
my $self = shift; |
458 |
my $branch; |
459 |
|
460 |
if ($self->{prefs}->{CircControl} eq "PickupLibrary" and $self->{checkout}) { |
461 |
$branch = $self->{checkout}->branchcode; # Why C4::Context->userenv->{'branch'} in C4::Circulation? |
462 |
} elsif ($self->{prefs}->{CircControl} eq "PatronLibrary") { |
463 |
$branch = $self->{patron}->branchcode; |
464 |
} else { |
465 |
my $branchfield = $self->{prefs}->{HomeOrHoldingBranch} || "homebranch"; |
466 |
$branch = $self->{item}->{$branchfield} || $self->{item}->{homebranch}; |
467 |
} |
468 |
return $branch; |
469 |
} |
470 |
|
471 |
=head $self->fixAccountForLostAndReturned |
472 |
If item lost, refund and mark as lost returned |
473 |
=cut |
474 |
sub fixAccountForLostAndReturned { |
475 |
my $self = shift; |
476 |
my $acc = Koha::Account::Lines->search( |
477 |
{ |
478 |
itemnumber => $self->{item}->itemnumber, |
479 |
accounttype => { -in => [ "L", "Rep" ] }, |
480 |
}, |
481 |
{ |
482 |
order_by => { -desc => [ "date", "accountno" ] } |
483 |
} |
484 |
)->next(); |
485 |
return unless $acc; |
486 |
|
487 |
$acc->accounttype("LR"); |
488 |
$acc->store(); |
489 |
|
490 |
my $account = Koha::Account->new( { patron_id => $acc->borrowernumber } ); |
491 |
my $credit_id = $account->pay( |
492 |
{ |
493 |
amount => $acc->amount, |
494 |
description => "Item Returned " . $self->{item}->barcode, |
495 |
account_type => "CR", |
496 |
offset_type => "Lost Item Return", |
497 |
accounlines => [$acc], |
498 |
|
499 |
} |
500 |
); |
501 |
|
502 |
return $credit_id; |
503 |
} |
504 |
|
505 |
=head |
506 |
ref C4::Circulation.pm L2369 |
507 |
From what I could decipher, this sets fine accounttype to F in normal cases, rest is handled in cronjobs |
508 |
TODO: |
509 |
if params exemptfine -> add line with credit and type "Forgiven" |
510 |
if params dropbox -> add line with credit and type "Dropbox" |
511 |
|
512 |
=cut |
513 |
sub fixOverduesOnReturn { |
514 |
my $self = shift; |
515 |
|
516 |
$self->{patron} and $self->{item} or croak "Missing patron or item"; |
517 |
|
518 |
my $accountline = Koha::Account::Lines->search( |
519 |
{ |
520 |
borrowernumber => $self->{patron}->borrowernumber, |
521 |
itemnumber => $self->{item}->itemnumber, |
522 |
-or => [ |
523 |
accounttype => "FU", |
524 |
accounttype => "O", |
525 |
], |
526 |
} |
527 |
)->next(); |
528 |
$accountline and $accountline->accounttype("F")->store; |
529 |
return $self; |
530 |
} |
531 |
|
532 |
=head |
533 |
ref C4::Circulation L676 |
534 |
|
535 |
=cut |
536 |
sub canItemBeIssued { |
537 |
my ($self, $dateDue, $ignoreReserves) = @_; |
538 |
# my ( $borrower, $barcode, $duedate, $inprocess, $ignore_reserves, $params ) = @_; |
539 |
my $needsconfirmation; # filled with problems that needs confirmations |
540 |
my $issuingimpossible; # filled with problems that causes the issue to be IMPOSSIBLE |
541 |
my $alerts; # filled with messages that shouldn't stop issuing, but the librarian should be aware of. |
542 |
my $messages; # filled with information messages-> that should be displayed. |
543 |
|
544 |
# my $onsite_checkout = $params->{onsite_checkout} || 0; |
545 |
# my $override_high_holds = $params->{override_high_holds} || 0; |
546 |
|
547 |
# PREFS |
548 |
# noissuescharge |
549 |
# AllowFineOverride |
550 |
# AllFinesNeedOverride |
551 |
# NoIssuesChargeGuarantees |
552 |
# This should be returned before calling Core::Circulation |
553 |
$self->{item}->itemnumber or return ($self, {UNKNOWN_BARCODE => 1}); |
554 |
# TODO: should dateDue be automatically calculated? |
555 |
# Makes no sense to ask for dateDue |
556 |
# unless ($dateDue) { |
557 |
# my $itype = $self->{item}->effective_itemtype; |
558 |
# $dateDue = C4::Circulation::CalcDateDue( _today(), $itype, $self->{library}->branchcode, $self->{patron}->unblessed, 1 ); |
559 |
|
560 |
# } |
561 |
# $dateDue->truncate( to => "minute" ); |
562 |
|
563 |
# PATRON FLAGS |
564 |
$self->{patron}->gonenoaddress and $issuingimpossible->{GNA} = 1; |
565 |
$self->{patron}->lost and $issuingimpossible->{CARD_LOST} = 1; |
566 |
$self->{patron}->debarred and $issuingimpossible->{DEBARRED} = 1; |
567 |
$self->{patron}->is_expired and $issuingimpossible->{EXPIRED} = 1; |
568 |
|
569 |
# DEBTS |
570 |
# Note: Koha::Account is different than other Koha Objects |
571 |
my $debt = Koha::Account->new({patron_id => $self->{patron}->borrowernumber})->balance; |
572 |
if ($debt > 0) { |
573 |
# TODO: if $self->{prefs}->{IssuingInProcess} |
574 |
if ($debt > $self->{prefs}->{noissuescharge} and not $self->{prefs}->{AllowFineOverride}) { |
575 |
$issuingimpossible->{DEBT} = sprintf( "%.2f", $debt ); |
576 |
} |
577 |
} elsif ($self->{prefs}->{NoIssuesChargeGuarantees}) { |
578 |
my $guarantees_charges; |
579 |
foreach my $g ( @$self->{patron}->guarantees ) { |
580 |
$guarantees_charges += Koha::Account->new({patron_id => $g->borrowernumber})->balance; |
581 |
} |
582 |
if ( $guarantees_charges > $self->{prefs}->{NoIssuesChargeGuarantees} and not $self->{prefs}->{AllowFineOverride}) { |
583 |
$issuingimpossible->{DEBT_GUARANTEES} = sprintf( "%.2f", $$guarantees_charges ); |
584 |
} |
585 |
} |
586 |
|
587 |
# DEBARRED |
588 |
if ( my $debarred_date = $self->{patron}->is_debarred ) { |
589 |
if ($debarred_date eq "9999-12-31") { |
590 |
$issuingimpossible->{USERBLOCKEDNOENDDATE} = $debarred_date; |
591 |
} else { |
592 |
$issuingimpossible->{USERBLOCKEDWITHENDDATE} = $debarred_date; |
593 |
} |
594 |
} elsif ( my $num_overdues = $self->{patron}->has_overdues ) { |
595 |
if ( C4::Context->preference("OverduesBlockCirc") eq "block") { |
596 |
$issuingimpossible->{USERBLOCKEDOVERDUE} = $num_overdues; |
597 |
} |
598 |
elsif ( C4::Context->preference("OverduesBlockCirc") eq "confirmation") { |
599 |
$needsconfirmation->{USERBLOCKEDOVERDUE} = $num_overdues; |
600 |
} |
601 |
} |
602 |
|
603 |
# ALREADY CHECKED OUT |
604 |
if ($self->{checkout}) { |
605 |
# SAME PATRON |
606 |
if ($self->{checkout}->borrowernumber eq $self->{patron}->borrowernumber ) { |
607 |
if ( $self->{checkout}->onsite_checkout and $self->{prefs}->{SwitchOnSiteCheckouts}) { |
608 |
$messages->{ONSITE_CHECKOUT_WILL_BE_SWITCHED} = 1; |
609 |
} else { |
610 |
my ($ok, $error) = $self->canItemBeRenewed(); |
611 |
if ($ok) { |
612 |
if ( $error eq "onsite_checkout" ) { |
613 |
$issuingimpossible->{NO_RENEWAL_FOR_ONSITE_CHECKOUTS} = 1; |
614 |
} else { |
615 |
$issuingimpossible->{NO_MORE_RENEWALS} = 1; |
616 |
} |
617 |
} else { |
618 |
$needsconfirmation->{RENEW_ISSUE} = 1; |
619 |
} |
620 |
} |
621 |
# DIFFERENT PATRON |
622 |
} else { |
623 |
my ( $ok, $message ) = $self->canItemBeReturned(); |
624 |
if ( $ok ) { |
625 |
$needsconfirmation->{ISSUED_TO_ANOTHER} = 1; |
626 |
# THESE ARE JUST SILLY - should be removed |
627 |
$needsconfirmation->{issued_firstname} = $self->{checkout}->patron->firstname; |
628 |
$needsconfirmation->{issued_surname} = $self->{checkout}->patron->surname; |
629 |
$needsconfirmation->{issued_cardnumber} = $self->{checkout}->patron->cardnumber; |
630 |
$needsconfirmation->{issued_borrowernumber} = $self->{checkout}->patron->borrowernumber; |
631 |
} else { |
632 |
$issuingimpossible->{RETURN_IMPOSSIBLE} = 1; |
633 |
$issuingimpossible->{branch_to_return} = $message; |
634 |
} |
635 |
} |
636 |
} |
637 |
|
638 |
# TODO: Too Many checkouts |
639 |
# JB34 CHECKS IF BORROWERS DON'T HAVE ISSUE TOO MANY BOOKS |
640 |
# |
641 |
# my $switch_onsite_checkout = ( |
642 |
# C4::Context->preference('SwitchOnSiteCheckouts') |
643 |
# and $issue |
644 |
# and $issue->onsite_checkout |
645 |
# and $issue->borrowernumber == $borrower->{'borrowernumber'} ? 1 : 0 ); |
646 |
# my $toomany = TooMany( $borrower, $item->{biblionumber}, $item, { onsite_checkout => $onsite_checkout, switch_onsite_checkout => $switch_onsite_checkout, } ); |
647 |
# # if TooMany max_allowed returns 0 the user doesn't have permission to check out this book |
648 |
# if ( $toomany && not exists $needsconfirmation->{RENEW_ISSUE} ) { |
649 |
# if ( $toomany->{max_allowed} == 0 ) { |
650 |
# $needsconfirmation->{PATRON_CANT} = 1; |
651 |
# } |
652 |
# if ( C4::Context->preference("AllowTooManyOverride") ) { |
653 |
# $needsconfirmation->{TOO_MANY} = $toomany->{reason}; |
654 |
# $needsconfirmation->{current_loan_count} = $toomany->{count}; |
655 |
# $needsconfirmation->{max_loans_allowed} = $toomany->{max_allowed}; |
656 |
# } else { |
657 |
# $issuingimpossible->{TOO_MANY} = $toomany->{reason}; |
658 |
# $issuingimpossible->{current_loan_count} = $toomany->{count}; |
659 |
# $issuingimpossible->{max_loans_allowed} = $toomany->{max_allowed}; |
660 |
# } |
661 |
# } |
662 |
|
663 |
# CHECKPREVCHECKOUT: CHECK IF ITEM HAS EVER BEEN LENT TO PATRON |
664 |
if ($self->{patron}->wants_check_for_previous_checkout and $self->{patron}->do_check_for_previous_checkout($self->{item})) { |
665 |
$needsconfirmation->{PREVISSUE} = 1 |
666 |
} |
667 |
|
668 |
# ITEM BASED RESTRICTIONS |
669 |
# TODO: |
670 |
# Age restriction |
671 |
# pref item-level_itypes |
672 |
# independentBranches |
673 |
# rental charges |
674 |
# decreaseLoanHighHolds |
675 |
|
676 |
$self->{item}->withdrawn > 0 and $issuingimpossible->{WTHDRAWN} = 1; |
677 |
if ($self->{item}->restricted and $self->{item}->restricted > 0) { |
678 |
$issuingimpossible->{RESTRICTED} = 1; |
679 |
} |
680 |
|
681 |
if ($self->{item}->notforloan) { |
682 |
if ($self->{prefs}->{AllowNotForLoanOverride}) { |
683 |
$issuingimpossible->{NOT_FOR_LOAN} = 1; |
684 |
$issuingimpossible->{item_notforloan} = $self->{item}->notforloan; # WHY THIS? |
685 |
} else { |
686 |
$needsconfirmation->{NOT_FOR_LOAN_FORCING} = 1; |
687 |
$needsconfirmation->{item_notforloan} = $self->{item}->notforloan; # WHY THIS? |
688 |
} |
689 |
} |
690 |
|
691 |
if ( $self->{item}->itemlost and $self->{prefs}->{IssueLostItem} ne "nothing" ) { |
692 |
my $av = Koha::AuthorisedValues->search({ category => 'LOST', authorised_value => $self->{item}->itemlost }); |
693 |
my $code = $av->count ? $av->next->lib : ""; |
694 |
( $self->{prefs}->{IssueLostItem} eq "confirm" ) and $needsconfirmation->{ITEM_LOST} = $code; |
695 |
( $self->{prefs}->{IssueLostItem} eq "alert" ) and $alerts->{ITEM_LOST} = $code; # TODO: alert only used once, should be removed |
696 |
} |
697 |
|
698 |
unless ( $ignoreReserves ) { |
699 |
my $hold = $self->getPendingHold(); |
700 |
if ($hold) { |
701 |
if ($hold->is_waiting) { |
702 |
$needsconfirmation->{RESERVE_WAITING} = 1; |
703 |
$needsconfirmation->{'reswaitingdate'} = $hold->waitingdate; |
704 |
} else { |
705 |
$needsconfirmation->{RESERVED} = 1; |
706 |
} |
707 |
|
708 |
$needsconfirmation->{'resfirstname'} = $hold->patron->firstname; |
709 |
$needsconfirmation->{'ressurname'} = $hold->patron->surname; |
710 |
$needsconfirmation->{'rescardnumber'} = $hold->patron->cardnumber; |
711 |
$needsconfirmation->{'resborrowernumber'} = $hold->patron->borrowernumber; |
712 |
$needsconfirmation->{'resbranchcode'} = $hold->patron->branchcode; |
713 |
$needsconfirmation->{'resreservedate'} = $hold->reservedate; |
714 |
} |
715 |
} |
716 |
|
717 |
# TODO: consider if this is neccessary |
718 |
if ( not $self->{prefs}->{AllowMultipleIssuesOnABiblio} and not $issuingimpossible->{NO_MORE_RENEWALS} and not $needsconfirmation->{RENEW_ISSUE}) { |
719 |
# don't do the multiple loans per bib check if we've |
720 |
# already determined that we've got a loan on the same item |
721 |
unless ($self->{item}->biblio->subscriptions->count) { |
722 |
my $checkouts = Koha::Checkouts->search({ |
723 |
borrowernumber => $self->{patron}->borrowernumber, |
724 |
biblionumber => $self->{item}->biblionumber, |
725 |
}, { |
726 |
join => 'item', |
727 |
}); |
728 |
if ( $checkouts->count ) { |
729 |
$needsconfirmation->{BIBLIO_ALREADY_ISSUED} = 1; |
730 |
} |
731 |
} |
732 |
} |
733 |
|
734 |
return ( $issuingimpossible, $needsconfirmation, $alerts, $messages ); |
735 |
} |
736 |
|
737 |
=head |
738 |
$self->canItemBeRenewed |
739 |
if no holds binds item, allow renewal |
740 |
args: |
741 |
override => bool |
742 |
return: |
743 |
bool => can/cannot be renewed |
744 |
msg => legacy error msg string |
745 |
=cut |
746 |
|
747 |
sub canItemBeRenewed { |
748 |
my ($self, $override) = @_; |
749 |
$self->{checkout} or return ( 0, "no_checkout" ); |
750 |
|
751 |
# PREFS THAT MIGHT APPLY |
752 |
# OverduesBlockRenewing |
753 |
# RestrictionBlockRenewing |
754 |
|
755 |
# Too few items to fill holds means item cannot be renewed |
756 |
if (not $self->canItemFillHold and not $override) { |
757 |
return ( 0, "on_hold" ); |
758 |
} |
759 |
|
760 |
# CIRCULATION RULES |
761 |
my $circ_rule = Koha::IssuingRules->get_effective_issuing_rule( |
762 |
{ categorycode => $self->{patron}->categorycode, |
763 |
itemtype => $self->{item}->itype, |
764 |
branchcode => $self->{checkout}->branchcode, |
765 |
} |
766 |
); |
767 |
|
768 |
my $renewals = $self->{checkout}->renewals || 0; |
769 |
$circ_rule or return ( 0, "too_many" ); # MISSING CIRC RULE - too_many in original, but should perhaps be better worded |
770 |
$circ_rule->renewalsallowed <= $renewals or return ( 0, "too_many" ); |
771 |
|
772 |
|
773 |
$self->{patron}->is_debarred and return ( 0, "restriction" ); |
774 |
($self->{patron}->has_overdues or $self->{checkout}->is_overdue) and return ( 0, "overdue" ); |
775 |
|
776 |
# TODO: AUTORENEW - USE CASE? |
777 |
|
778 |
# NO RENEWAL BEFORE |
779 |
if ( defined $circ_rule->norenewalbefore and $circ_rule->norenewalbefore ne "" ) { |
780 |
my $dt = _mysqldate2dt($self->{checkout}->date_due); |
781 |
my $soonestrenewal = $dt->subtract( $circ_rule->lengthunit => $circ_rule->norenewalbefore ); |
782 |
if ( $soonestrenewal > DateTime->now() ) { |
783 |
return ( 0, "too_soon" ); |
784 |
} |
785 |
} |
786 |
return (1, undef); |
787 |
} |
788 |
|
789 |
sub canItemBeReturned { |
790 |
my $self = shift; |
791 |
my $allowreturntobranch = $self->{prefs}->{AllowReturnToBranch}; |
792 |
|
793 |
# assume return is allowed to start |
794 |
my $allowed = 1; |
795 |
my $message; |
796 |
|
797 |
# identify all cases where return is forbidden |
798 |
if ($allowreturntobranch eq "homebranch" && $self->{library}->branchcode ne $self->{item}->homebranch) { |
799 |
$allowed = 0; |
800 |
$message = $self->{item}->homebranch; |
801 |
} elsif ($allowreturntobranch eq "holdingbranch" && $self->{library}->branchcode ne $self->{item}->holdingbranch) { |
802 |
$allowed = 0; |
803 |
$message = $self->{item}->holdingbranch; |
804 |
} elsif ($allowreturntobranch eq "homeorholdingbranch" && $self->{library}->branchcode ne $self->{item}->homebranch |
805 |
&& $self->{library}->branchcode ne $self->{item}->holdingbranch) { |
806 |
$allowed = 0; |
807 |
$message = $self->{item}->homebranch; # FIXME: choice of homebranch is arbitrary |
808 |
} |
809 |
|
810 |
return ($allowed, $message); |
811 |
} |
812 |
|
813 |
=head |
814 |
Check if item can fill a pending hold |
815 |
reservable means: |
816 |
- item not found (Waiting or in Transfer) |
817 |
- item not lost, withdrawn |
818 |
- item notforloan not code > 0 |
819 |
- item not damaged (unless AllowHoldsOnDamagedItems) |
820 |
- number of reservable items is greater than number of pending holds |
821 |
- something something onshelfholds |
822 |
|
823 |
ref: C4::Reserves::CanItemBeReserved L282 |
824 |
=cut |
825 |
sub canItemFillHold { |
826 |
my $self = shift; |
827 |
|
828 |
# PREFS |
829 |
# AllowRenewalIfOtherItemsAvailable |
830 |
# item-level_itypes |
831 |
# IndependentBranches |
832 |
# canreservefromotherbranches |
833 |
# AllowHoldsOnDamagedItems |
834 |
|
835 |
my $reservableItems = Koha::Items->search({ |
836 |
"me.biblionumber" => $self->{item}->biblionumber, |
837 |
itemlost => 0, |
838 |
withdrawn => 0, |
839 |
notforloan => { "<=" => 0 }, |
840 |
"reserves.found" => { "!=" => undef }, |
841 |
}, |
842 |
{ join => "reserves" }); |
843 |
|
844 |
if ($reservableItems->count < $self->getPendingHoldsByBiblio->count) { |
845 |
return 0; |
846 |
} else { |
847 |
return 1; |
848 |
} |
849 |
} |
850 |
=head |
851 |
return next hold, item or biblio level |
852 |
=cut |
853 |
sub getPendingHold { |
854 |
my $self = shift; |
855 |
my $itemholds = $self->{item}->current_holds; |
856 |
my $biblioholds = Koha::Holds->search({biblionumber => $self->{item}->biblionumber, found => undef, suspend => 0}, { order_by => { -asc => "priority" }}); |
857 |
my $hold = $itemholds->count ? $itemholds->next : $biblioholds->count ? $biblioholds->next : undef; |
858 |
return $hold; |
859 |
} |
860 |
|
861 |
=head |
862 |
return pending holds on biblio level |
863 |
=cut |
864 |
sub getPendingHoldsByBiblio { |
865 |
my $self = shift; |
866 |
return Koha::Holds->search({biblionumber => $self->{item}->biblionumber, found => undef, suspend => 0}, { order_by => { -asc => "priority" }}); |
867 |
} |
868 |
|
869 |
sub _mysqldate2dt { |
870 |
my $datestring = shift; |
871 |
my $strp = DateTime::Format::Strptime->new( pattern => "%Y-%m-%d %H:%M:%S" ); |
872 |
return $strp->parse_datetime( $datestring ); |
873 |
} |
874 |
|
875 |
sub _dt2mysqldate { |
876 |
my $dt = shift; |
877 |
if (ref $dt ne "DateTime") { |
878 |
return $dt; |
879 |
} else { |
880 |
return $dt->strftime("%F %T"); |
881 |
} |
882 |
} |
883 |
|
884 |
sub _ymd2dt { |
885 |
my $ymdstring = shift; |
886 |
my $strp = DateTime::Format::Strptime->new( pattern => "%Y-%m-%d" ); |
887 |
return $strp->parse_datetime( $ymdstring ); |
888 |
} |
889 |
|
890 |
sub _today { |
891 |
return DateTime->now()->ymd; |
892 |
} |
893 |
|
894 |
sub sendCirculationAlert { |
895 |
# TODO |
896 |
} |
897 |
|
898 |
=head |
899 |
Remove any OVERDUES related debarment if the borrower has no overdues |
900 |
=cut |
901 |
sub removeOverdueDebarments { |
902 |
my $self = shift; |
903 |
return unless $self->{patron}; |
904 |
if ( $self->{patron}->is_debarred |
905 |
&& ! $self->{patron}->has_overdues |
906 |
&& @{ Koha::Patron::Debarments::GetDebarments({ borrowernumber => $self->{patron}->borrowernumber, type => "OVERDUES" }) } |
907 |
) { |
908 |
Koha::Patron::Debarments::DelUniqueDebarment({ borrowernumber => $self->{patron}->borrowernumber, type => "OVERDUES" }); |
909 |
} |
910 |
} |
911 |
|
912 |
1; |