Bug 19532: Make recalls.status an ENUM
[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 qw( dt_from_string );
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 })->as_list;
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     return $self->status eq 'requested';
146 }
147
148 =head3 waiting
149
150     if ( $recall->waiting )
151
152     [% IF recall.waiting %]
153
154 Return true if recall is awaiting pickup.
155
156 =cut
157
158 sub waiting {
159     my ( $self ) = @_;
160     return $self->status eq 'waiting';
161 }
162
163 =head3 overdue
164
165     if ( $recall->overdue )
166
167     [% IF recall.overdue %]
168
169 Return true if recall is overdue to be returned.
170
171 =cut
172
173 sub overdue {
174     my ( $self ) = @_;
175     return $self->status eq 'overdue';
176 }
177
178 =head3 in_transit
179
180     if ( $recall->in_transit )
181
182     [% IF recall.in_transit %]
183
184 Return true if recall is in transit.
185
186 =cut
187
188 sub in_transit {
189     my ( $self ) = @_;
190     return $self->status eq 'in_transit';
191 }
192
193 =head3 expired
194
195     if ( $recall->expired )
196
197     [% IF recall.expired %]
198
199 Return true if recall has expired.
200
201 =cut
202
203 sub expired {
204     my ( $self ) = @_;
205     return $self->status eq 'expired';
206 }
207
208 =head3 cancelled
209
210     if ( $recall->cancelled )
211
212     [% IF recall.cancelled %]
213
214 Return true if recall has been cancelled.
215
216 =cut
217
218 sub cancelled {
219     my ( $self ) = @_;
220     return $self->status eq 'cancelled';
221 }
222
223 =head3 fulfilled
224
225     if ( $recall->fulfilled )
226
227     [% IF recall.fulfilled %]
228
229 Return true if the recall has been fulfilled.
230
231 =cut
232
233 sub fulfilled {
234     my ( $self ) = @_;
235     return $self->status eq 'fulfilled';
236 }
237
238 =head3 calc_expirationdate
239
240     my $expirationdate = $recall->calc_expirationdate;
241     $recall->update({ expirationdate => $expirationdate });
242
243 Calculate the expirationdate to set based on circulation rules and system preferences.
244
245 =cut
246
247 sub calc_expirationdate {
248     my ( $self ) = @_;
249
250     my $item;
251     if ( $self->item_level_recall ) {
252         $item = $self->item;
253     } elsif ( $self->checkout ) {
254         $item = $self->checkout->item;
255     }
256
257     my $branchcode = $self->patron->branchcode;
258     if ( $item ) {
259         $branchcode = C4::Circulation::_GetCircControlBranch( $item->unblessed, $self->patron->unblessed );
260     }
261
262     my $rule = Koha::CirculationRules->get_effective_rule({
263         categorycode => $self->patron->categorycode,
264         branchcode => $branchcode,
265         itemtype => $item ? $item->effective_itemtype : undef,
266         rule_name => 'recall_shelf_time'
267     });
268
269     my $shelf_time = defined $rule ? $rule->rule_value : C4::Context->preference('RecallsMaxPickUpDelay');
270
271     my $expirationdate = dt_from_string->add( days => $shelf_time );
272     return $expirationdate;
273 }
274
275 =head3 start_transfer
276
277     my ( $recall, $dotransfer, $messages ) = $recall->start_transfer({ item => $item_object });
278
279 Set the recall as in transit.
280
281 =cut
282
283 sub start_transfer {
284     my ( $self, $params ) = @_;
285
286     if ( $self->item_level_recall ) {
287         # already has an itemnumber
288         $self->update({ status => 'in_transit' });
289     } else {
290         my $itemnumber = $params->{item}->itemnumber;
291         $self->update({ status => 'in_transit', itemnumber => $itemnumber });
292     }
293
294     my ( $dotransfer, $messages ) = C4::Circulation::transferbook({ to_branch => $self->branchcode, from_branch => $self->item->holdingbranch, barcode => $self->item->barcode, trigger => 'Recall' });
295
296     return ( $self, $dotransfer, $messages );
297 }
298
299 =head3 revert_transfer
300
301     $recall->revert_transfer;
302
303 If a transfer is cancelled, revert the recall to requested.
304
305 =cut
306
307 sub revert_transfer {
308     my ( $self ) = @_;
309
310     if ( $self->item_level_recall ) {
311         $self->update({ status => 'requested' });
312     } else {
313         $self->update({ status => 'requested', itemnumber => undef });
314     }
315
316     return $self;
317 }
318
319 =head3 set_waiting
320
321     $recall->set_waiting(
322         {   expirationdate => $expirationdate,
323             item           => $item_object
324         }
325     );
326
327 Set the recall as waiting and update expiration date.
328 Notify the recall requester.
329
330 =cut
331
332 sub set_waiting {
333     my ( $self, $params ) = @_;
334
335     my $itemnumber;
336     if ( $self->item_level_recall ) {
337         $itemnumber = $self->itemnumber;
338         $self->update({ status => 'waiting', waitingdate => dt_from_string, expirationdate => $params->{expirationdate} });
339     } else {
340         # biblio-level recall with no itemnumber. need to set itemnumber
341         $itemnumber = $params->{item}->itemnumber;
342         $self->update({ status => 'waiting', waitingdate => dt_from_string, expirationdate => $params->{expirationdate}, itemnumber => $itemnumber });
343     }
344
345     # send notice to recaller to pick up item
346     my $letter = C4::Letters::GetPreparedLetter(
347         module => 'circulation',
348         letter_code => 'PICKUP_RECALLED_ITEM',
349         branchcode => $self->branchcode,
350         want_librarian => 0,
351         tables => {
352             biblio => $self->biblionumber,
353             borrowers => $self->borrowernumber,
354             items => $itemnumber,
355             recalls => $self->recall_id,
356         },
357     );
358
359     C4::Message->enqueue($letter, $self->patron->unblessed, 'email');
360
361     return $self;
362 }
363
364 =head3 revert_waiting
365
366     $recall->revert_waiting;
367
368 Revert recall waiting status.
369
370 =cut
371
372 sub revert_waiting {
373     my ( $self ) = @_;
374     if ( $self->item_level_recall ){
375         $self->update({ status => 'requested', waitingdate => undef });
376     } else {
377         $self->update({ status => 'requested', waitingdate => undef, itemnumber => undef });
378     }
379     return $self;
380 }
381
382 =head3 should_be_overdue
383
384     if ( $recall->should_be_overdue ) {
385         $recall->set_overdue;
386     }
387
388 Return true if this recall should be marked overdue
389
390 =cut
391
392 sub should_be_overdue {
393     my ( $self ) = @_;
394     if ( $self->requested and $self->checkout and dt_from_string( $self->checkout->date_due ) <= dt_from_string ) {
395         return 1;
396     }
397     return 0;
398 }
399
400 =head3 set_overdue
401
402     $recall->set_overdue;
403
404 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.
405
406 =cut
407
408 sub set_overdue {
409     my ( $self, $params ) = @_;
410     my $interface = $params->{interface} || 'COMMANDLINE';
411     $self->update({ status => 'overdue' });
412     C4::Log::logaction( 'RECALLS', 'OVERDUE', $self->recall_id, "Recall status set to overdue", $interface ) if ( C4::Context->preference('RecallsLog') );
413     return $self;
414 }
415
416 =head3 set_expired
417
418     $recall->set_expired({ interface => 'INTRANET' });
419
420 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.
421
422 =cut
423
424 sub set_expired {
425     my ( $self, $params ) = @_;
426     my $interface = $params->{interface} || 'COMMANDLINE';
427     $self->update({ status => 'expired', old => 1, expirationdate => dt_from_string });
428     C4::Log::logaction( 'RECALLS', 'EXPIRE', $self->recall_id, "Recall expired", $interface ) if ( C4::Context->preference('RecallsLog') );
429     return $self;
430 }
431
432 =head3 set_cancelled
433
434     $recall->set_cancelled;
435
436 Set a recall as cancelled. This may be done manually, either by the borrower that placed the recall, or by the library.
437
438 =cut
439
440 sub set_cancelled {
441     my ( $self ) = @_;
442     $self->update({ status => 'cancelled', old => 1, cancellationdate => dt_from_string });
443     C4::Log::logaction( 'RECALLS', 'CANCEL', $self->recall_id, "Recall cancelled", 'INTRANET' ) if ( C4::Context->preference('RecallsLog') );
444     return $self;
445 }
446
447 =head3 set_fulfilled
448
449     $recall->set_fulfilled;
450
451 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.
452
453 =cut
454
455 sub set_fulfilled {
456     my ( $self ) = @_;
457     $self->update({ status => 'fulfilled', old => 1 });
458     C4::Log::logaction( 'RECALLS', 'FULFILL', $self->recall_id, "Recall fulfilled", 'INTRANET' ) if ( C4::Context->preference('RecallsLog') );
459     return $self;
460 }
461
462 =head3 _type
463
464 =cut
465
466 sub _type {
467     return 'Recall';
468 }
469
470 1;