Bug 19532: (follow-up) aria-hidden attr on OPAC, and more
[koha.git] / Koha / Recall.pm
1 package Koha::Recall;
2
3 # Copyright 2020 Aleisha Amohia <aleisha@catalyst.net.nz>
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use Modern::Perl;
21
22 use Koha::Database;
23 use Koha::DateUtils;
24 use Koha::Patron;
25 use Koha::Biblio;
26 use Koha::Item;
27
28 use base qw(Koha::Object);
29
30 =head1 NAME
31
32 Koha::Recall - Koha Recall Object class
33
34 =head1 API
35
36 =head2 Internal methods
37
38 =cut
39
40 =head3 biblio
41
42     my $biblio = $recall->biblio;
43
44 Returns the related Koha::Biblio object for this recall.
45
46 =cut
47
48 sub biblio {
49     my ( $self ) = @_;
50     my $biblio_rs = $self->_result->biblio;
51     return unless $biblio_rs;
52     return Koha::Biblio->_new_from_dbic( $biblio_rs );
53 }
54
55 =head3 item
56
57     my $item = $recall->item;
58
59 Returns the related Koha::Item object for this recall.
60
61 =cut
62
63 sub item {
64     my ( $self ) = @_;
65     my $item_rs = $self->_result->item;
66     return unless $item_rs;
67     return Koha::Item->_new_from_dbic( $item_rs );
68 }
69
70 =head3 patron
71
72     my $patron = $recall->patron;
73
74 Returns the related Koha::Patron object for this recall.
75
76 =cut
77
78 sub patron {
79     my ( $self ) = @_;
80     my $patron_rs = $self->_result->borrower;
81     return unless $patron_rs;
82     return Koha::Patron->_new_from_dbic( $patron_rs );
83 }
84
85 =head3 library
86
87     my $library = $recall->library;
88
89 Returns the related Koha::Library object for this recall.
90
91 =cut
92
93 sub library {
94     my ( $self ) = @_;
95     $self->{_library} = Koha::Libraries->find( $self->branchcode );
96     return $self->{_library};
97 }
98
99 =head3 checkout
100
101     my $checkout = $recall->checkout;
102
103 Returns the related Koha::Checkout object for this recall.
104
105 =cut
106
107 sub checkout {
108     my ( $self ) = @_;
109     $self->{_checkout} ||= Koha::Checkouts->find({ itemnumber => $self->itemnumber });
110
111     unless ( $self->item_level_recall ) {
112         # Only look at checkouts of items that are allowed to be recalled, and get the oldest one
113         my @items = Koha::Items->search({ biblionumber => $self->biblionumber });
114         my @itemnumbers;
115         foreach (@items) {
116             my $recalls_allowed = Koha::CirculationRules->get_effective_rule({
117                 branchcode => C4::Context->userenv->{'branch'},
118                 categorycode => $self->patron->categorycode,
119                 itemtype => $_->effective_itemtype,
120                 rule_name => 'recalls_allowed',
121             });
122             if ( defined $recalls_allowed and $recalls_allowed->rule_value > 0 ) {
123                 push ( @itemnumbers, $_->itemnumber );
124             }
125         }
126         my $checkouts = Koha::Checkouts->search({ itemnumber => [ @itemnumbers ] }, { order_by => { -asc => 'date_due' } });
127         $self->{_checkout} = $checkouts->next;
128     }
129
130     return $self->{_checkout};
131 }
132
133 =head3 requested
134
135     if ( $recall->requested )
136
137     [% IF recall.requested %]
138
139 Return true if recall status is requested.
140
141 =cut
142
143 sub requested {
144     my ( $self ) = @_;
145     my $status = $self->status;
146     return $status && $status eq 'R';
147 }
148
149 =head3 waiting
150
151     if ( $recall->waiting )
152
153     [% IF recall.waiting %]
154
155 Return true if recall is awaiting pickup.
156
157 =cut
158
159 sub waiting {
160     my ( $self ) = @_;
161     my $status = $self->status;
162     return $status && $status eq 'W';
163 }
164
165 =head3 overdue
166
167     if ( $recall->overdue )
168
169     [% IF recall.overdue %]
170
171 Return true if recall is overdue to be returned.
172
173 =cut
174
175 sub overdue {
176     my ( $self ) = @_;
177     my $status = $self->status;
178     return $status && $status eq 'O';
179 }
180
181 =head3 in_transit
182
183     if ( $recall->in_transit )
184
185     [% IF recall.in_transit %]
186
187 Return true if recall is in transit.
188
189 =cut
190
191 sub in_transit {
192     my ( $self ) = @_;
193     my $status = $self->status;
194     return $status && $status eq 'T';
195 }
196
197 =head3 expired
198
199     if ( $recall->expired )
200
201     [% IF recall.expired %]
202
203 Return true if recall has expired.
204
205 =cut
206
207 sub expired {
208     my ( $self ) = @_;
209     my $status = $self->status;
210     return $status && $status eq 'E';
211 }
212
213 =head3 cancelled
214
215     if ( $recall->cancelled )
216
217     [% IF recall.cancelled %]
218
219 Return true if recall has been cancelled.
220
221 =cut
222
223 sub cancelled {
224     my ( $self ) = @_;
225     my $status = $self->status;
226     return $status && $status eq 'C';
227 }
228
229 =head3 finished
230
231     if ( $recall->finished )
232
233     [% IF recall.finished %]
234
235 Return true if recall is finished and has been fulfilled.
236
237 =cut
238
239 sub finished {
240     my ( $self ) = @_;
241     my $status = $self->status;
242     return $status && $status eq 'F';
243 }
244
245 =head3 calc_expirationdate
246
247     my $expirationdate = $recall->calc_expirationdate;
248     $recall->update({ expirationdate => $expirationdate });
249
250 Calculate the expirationdate to set based on circulation rules and system preferences.
251
252 =cut
253
254 sub calc_expirationdate {
255     my ( $self ) = @_;
256
257     my $item;
258     if ( $self->item_level_recall ) {
259         $item = $self->item;
260     } elsif ( $self->checkout ) {
261         $item = $self->checkout->item;
262     }
263
264     my $branchcode = $self->patron->branchcode;
265     if ( $item ) {
266         $branchcode = C4::Circulation::_GetCircControlBranch( $item->unblessed, $self->patron->unblessed );
267     }
268
269     my $rule = Koha::CirculationRules->get_effective_rule({
270         categorycode => $self->patron->categorycode,
271         branchcode => $branchcode,
272         itemtype => $item ? $item->effective_itemtype : undef,
273         rule_name => 'recall_shelf_time'
274     });
275
276     my $shelf_time = defined $rule ? $rule->rule_value : C4::Context->preference('RecallsMaxPickUpDelay');
277
278     my $expirationdate = dt_from_string->add( days => $shelf_time );
279     return $expirationdate;
280 }
281
282 =head3 start_transfer
283
284     my ( $recall, $dotransfer, $messages ) = $recall->start_transfer({ item => $item_object });
285
286 Set the recall as in transit.
287
288 =cut
289
290 sub start_transfer {
291     my ( $self, $params ) = @_;
292
293     if ( $self->item_level_recall ) {
294         # already has an itemnumber
295         $self->update({ status => 'T' });
296     } else {
297         my $itemnumber = $params->{item}->itemnumber;
298         $self->update({ status => 'T', itemnumber => $itemnumber });
299     }
300
301     my ( $dotransfer, $messages ) = C4::Circulation::transferbook({ to_branch => $self->branchcode, from_branch => $self->item->holdingbranch, barcode => $self->item->barcode, trigger => 'Recall' });
302
303     return ( $self, $dotransfer, $messages );
304 }
305
306 =head3 revert_transfer
307
308     $recall->revert_transfer;
309
310 If a transfer is cancelled, revert the recall to requested.
311
312 =cut
313
314 sub revert_transfer {
315     my ( $self ) = @_;
316
317     if ( $self->item_level_recall ) {
318         $self->update({ status => 'R' });
319     } else {
320         $self->update({ status => 'R', itemnumber => undef });
321     }
322
323     return $self;
324 }
325
326 =head3 set_waiting
327
328     $recall->set_waiting({
329         expirationdate => $expirationdate,
330         item => $item_object
331     });
332
333 Set the recall as waiting and update expiration date.
334 Notify the recall requester.
335
336 =cut
337
338 sub set_waiting {
339     my ( $self, $params ) = @_;
340
341     my $itemnumber;
342     if ( $self->item_level_recall ) {
343         $itemnumber = $self->itemnumber;
344         $self->update({ status => 'W', waitingdate => dt_from_string, expirationdate => $params->{expirationdate} });
345     } else {
346         # biblio-level recall with no itemnumber. need to set itemnumber
347         $itemnumber = $params->{item}->itemnumber;
348         $self->update({ status => 'W', waitingdate => dt_from_string, expirationdate => $params->{expirationdate}, itemnumber => $itemnumber });
349     }
350
351     # send notice to recaller to pick up item
352     my $letter = C4::Letters::GetPreparedLetter(
353         module => 'circulation',
354         letter_code => 'PICKUP_RECALLED_ITEM',
355         branchcode => $self->branchcode,
356         want_librarian => 0,
357         tables => {
358             biblio => $self->biblionumber,
359             borrowers => $self->borrowernumber,
360             items => $itemnumber,
361             recalls => $self->recall_id,
362         },
363     );
364
365     C4::Message->enqueue($letter, $self->patron->unblessed, 'email');
366
367     return $self;
368 }
369
370 =head3 revert_waiting
371
372     $recall->revert_waiting;
373
374 Revert recall waiting status.
375
376 =cut
377
378 sub revert_waiting {
379     my ( $self ) = @_;
380     if ( $self->item_level_recall ){
381         $self->update({ status => 'R', waitingdate => undef });
382     } else {
383         $self->update({ status => 'R', waitingdate => undef, itemnumber => undef });
384     }
385     return $self;
386 }
387
388 =head3 should_be_overdue
389
390     if ( $recall->should_be_overdue ) {
391         $recall->set_overdue;
392     }
393
394 Return true if this recall should be marked overdue
395
396 =cut
397
398 sub should_be_overdue {
399     my ( $self ) = @_;
400     if ( $self->requested and $self->checkout and dt_from_string( $self->checkout->date_due ) <= dt_from_string ) {
401         return 1;
402     }
403     return 0;
404 }
405
406 =head3 set_overdue
407
408     $recall->set_overdue;
409
410 Set a recall as overdue when the recall has been requested and the borrower who has checked out the recalled item is late to return it. This can be done manually by the library or by cronjob. The interface is either 'INTRANET' or 'COMMANDLINE' for logging purposes.
411
412 =cut
413
414 sub set_overdue {
415     my ( $self, $params ) = @_;
416     my $interface = $params->{interface} || 'COMMANDLINE';
417     $self->update({ status => 'O' });
418     C4::Log::logaction( 'RECALLS', 'OVERDUE', $self->recall_id, "Recall status set to overdue", $interface ) if ( C4::Context->preference('RecallsLog') );
419     return $self;
420 }
421
422 =head3 set_expired
423
424     $recall->set_expired({ interface => 'INTRANET' });
425
426 Set a recall as expired. This may be done manually or by a cronjob, either when the borrower that placed the recall takes more than RecallsMaxPickUpDelay number of days to collect their item, or if the specified expirationdate passes. The interface is either 'INTRANET' or 'COMMANDLINE' for logging purposes.
427
428 =cut
429
430 sub set_expired {
431     my ( $self, $params ) = @_;
432     my $interface = $params->{interface} || 'COMMANDLINE';
433     $self->update({ status => 'E', old => 1, expirationdate => dt_from_string });
434     C4::Log::logaction( 'RECALLS', 'EXPIRE', $self->recall_id, "Recall expired", $interface ) if ( C4::Context->preference('RecallsLog') );
435     return $self;
436 }
437
438 =head3 set_cancelled
439
440     $recall->set_cancelled;
441
442 Set a recall as cancelled. This may be done manually, either by the borrower that placed the recall, or by the library.
443
444 =cut
445
446 sub set_cancelled {
447     my ( $self ) = @_;
448     $self->update({ status => 'C', old => 1, cancellationdate => dt_from_string });
449     C4::Log::logaction( 'RECALLS', 'CANCEL', $self->recall_id, "Recall cancelled", 'INTRANET' ) if ( C4::Context->preference('RecallsLog') );
450     return $self;
451 }
452
453 =head3 set_finished
454
455     $recall->set_finished;
456
457 Set a recall as finished. This should only be called when the item allocated to a recall is checked out to the borrower who requested the recall.
458
459 =cut
460
461 sub set_finished {
462     my ( $self ) = @_;
463     $self->update({ status => 'F', old => 1 });
464     C4::Log::logaction( 'RECALLS', 'FULFILL', $self->recall_id, "Recall fulfilled", 'INTRANET' ) if ( C4::Context->preference('RecallsLog') );
465     return $self;
466 }
467
468 =head3 _type
469
470 =cut
471
472 sub _type {
473     return 'Recall';
474 }
475
476 1;