Bug 20256: DBIC schema
[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::Biblios;
25 use Koha::Items;
26 use Koha::Libraries;
27 use Koha::Patrons;
28
29 use base qw(Koha::Object);
30
31 =head1 NAME
32
33 Koha::Recall - Koha Recall Object class
34
35 =head1 API
36
37 =head2 Class methods
38
39 =cut
40
41 =head3 biblio
42
43     my $biblio = $recall->biblio;
44
45 Returns the related Koha::Biblio object for this recall.
46
47 =cut
48
49 sub biblio {
50     my ( $self ) = @_;
51     my $biblio_rs = $self->_result->biblio;
52     return unless $biblio_rs;
53     return Koha::Biblio->_new_from_dbic( $biblio_rs );
54 }
55
56 =head3 item
57
58     my $item = $recall->item;
59
60 Returns the related Koha::Item object for this recall.
61
62 =cut
63
64 sub item {
65     my ( $self ) = @_;
66     my $item_rs = $self->_result->item;
67     return unless $item_rs;
68     return Koha::Item->_new_from_dbic( $item_rs );
69 }
70
71 =head3 patron
72
73     my $patron = $recall->patron;
74
75 Returns the related Koha::Patron object for this recall.
76
77 =cut
78
79 sub patron {
80     my ( $self ) = @_;
81     my $patron_rs = $self->_result->patron;
82     return unless $patron_rs;
83     return Koha::Patron->_new_from_dbic( $patron_rs );
84 }
85
86 =head3 library
87
88     my $library = $recall->library;
89
90 Returns the related Koha::Library object for this recall.
91
92 =cut
93
94 sub library {
95     my ( $self ) = @_;
96     my $library_rs = $self->_result->library;
97     return unless $library_rs;
98     return Koha::Library->_new_from_dbic( $library_rs );
99 }
100
101 =head3 checkout
102
103     my $checkout = $recall->checkout;
104
105 Returns the related Koha::Checkout object for this recall.
106
107 =cut
108
109 sub checkout {
110     my ( $self ) = @_;
111     $self->{_checkout} ||= Koha::Checkouts->find({ itemnumber => $self->item_id });
112
113     unless ( $self->item_level ) {
114         # Only look at checkouts of items that are allowed to be recalled, and get the oldest one
115         my @items = Koha::Items->search({ biblionumber => $self->biblio_id })->as_list;
116         my @itemnumbers;
117         foreach (@items) {
118             my $recalls_allowed = Koha::CirculationRules->get_effective_rule({
119                 branchcode => C4::Context->userenv->{'branch'},
120                 categorycode => $self->patron->categorycode,
121                 itemtype => $_->effective_itemtype,
122                 rule_name => 'recalls_allowed',
123             });
124             if ( defined $recalls_allowed and $recalls_allowed->rule_value > 0 ) {
125                 push ( @itemnumbers, $_->itemnumber );
126             }
127         }
128         my $checkouts = Koha::Checkouts->search({ itemnumber => [ @itemnumbers ] }, { order_by => { -asc => 'date_due' } });
129         $self->{_checkout} = $checkouts->next;
130     }
131
132     return $self->{_checkout};
133 }
134
135 =head3 requested
136
137     if ( $recall->requested )
138
139     [% IF recall.requested %]
140
141 Return true if recall status is requested.
142
143 =cut
144
145 sub requested {
146     my ( $self ) = @_;
147     return $self->status eq 'requested';
148 }
149
150 =head3 waiting
151
152     if ( $recall->waiting )
153
154     [% IF recall.waiting %]
155
156 Return true if recall is awaiting pickup.
157
158 =cut
159
160 sub waiting {
161     my ( $self ) = @_;
162     return $self->status eq 'waiting';
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     return $self->status eq 'overdue';
178 }
179
180 =head3 in_transit
181
182     if ( $recall->in_transit )
183
184     [% IF recall.in_transit %]
185
186 Return true if recall is in transit.
187
188 =cut
189
190 sub in_transit {
191     my ( $self ) = @_;
192     return $self->status eq 'in_transit';
193 }
194
195 =head3 expired
196
197     if ( $recall->expired )
198
199     [% IF recall.expired %]
200
201 Return true if recall has expired.
202
203 =cut
204
205 sub expired {
206     my ( $self ) = @_;
207     return $self->status eq 'expired';
208 }
209
210 =head3 cancelled
211
212     if ( $recall->cancelled )
213
214     [% IF recall.cancelled %]
215
216 Return true if recall has been cancelled.
217
218 =cut
219
220 sub cancelled {
221     my ( $self ) = @_;
222     return $self->status eq 'cancelled';
223 }
224
225 =head3 fulfilled
226
227     if ( $recall->fulfilled )
228
229     [% IF recall.fulfilled %]
230
231 Return true if the recall has been fulfilled.
232
233 =cut
234
235 sub fulfilled {
236     my ( $self ) = @_;
237     return $self->status eq 'fulfilled';
238 }
239
240 =head3 calc_expirationdate
241
242     my $expirationdate = $recall->calc_expirationdate;
243     $recall->update({ expirationdate => $expirationdate });
244
245 Calculate the expirationdate to set based on circulation rules and system preferences.
246
247 =cut
248
249 sub calc_expirationdate {
250     my ( $self ) = @_;
251
252     my $item;
253     if ( $self->item_level ) {
254         $item = $self->item;
255     } elsif ( $self->checkout ) {
256         $item = $self->checkout->item;
257     }
258
259     my $branchcode = $self->patron->branchcode;
260     if ( $item ) {
261         $branchcode = C4::Circulation::_GetCircControlBranch( $item->unblessed, $self->patron->unblessed );
262     }
263
264     my $rule = Koha::CirculationRules->get_effective_rule({
265         categorycode => $self->patron->categorycode,
266         branchcode => $branchcode,
267         itemtype => $item ? $item->effective_itemtype : undef,
268         rule_name => 'recall_shelf_time'
269     });
270
271     my $shelf_time = defined $rule ? $rule->rule_value : C4::Context->preference('RecallsMaxPickUpDelay');
272
273     my $expirationdate = dt_from_string->add( days => $shelf_time );
274     return $expirationdate;
275 }
276
277 =head3 start_transfer
278
279     my ( $recall, $dotransfer, $messages ) = $recall->start_transfer({ item => $item_object });
280
281 Set the recall as in transit.
282
283 =cut
284
285 sub start_transfer {
286     my ( $self, $params ) = @_;
287
288     if ( $self->item_level ) {
289         # already has an itemnumber
290         $self->update({ status => 'in_transit' });
291     } else {
292         my $itemnumber = $params->{item}->itemnumber;
293         $self->update({ status => 'in_transit', item_id => $itemnumber });
294     }
295
296     my ( $dotransfer, $messages ) = C4::Circulation::transferbook({ to_branch => $self->pickup_library_id, from_branch => $self->item->holdingbranch, barcode => $self->item->barcode, trigger => 'Recall' });
297
298     return ( $self, $dotransfer, $messages );
299 }
300
301 =head3 revert_transfer
302
303     $recall->revert_transfer;
304
305 If a transfer is cancelled, revert the recall to requested.
306
307 =cut
308
309 sub revert_transfer {
310     my ( $self ) = @_;
311
312     if ( $self->item_level ) {
313         $self->update({ status => 'requested' });
314     } else {
315         $self->update({ status => 'requested', item_id => undef });
316     }
317
318     return $self;
319 }
320
321 =head3 set_waiting
322
323     $recall->set_waiting(
324         {   expirationdate => $expirationdate,
325             item           => $item_object
326         }
327     );
328
329 Set the recall as waiting and update expiration date.
330 Notify the recall requester.
331
332 =cut
333
334 sub set_waiting {
335     my ( $self, $params ) = @_;
336
337     my $itemnumber;
338     if ( $self->item_level ) {
339         $itemnumber = $self->item_id;
340         $self->update({ status => 'waiting', waiting_date => dt_from_string, expiration_date => $params->{expirationdate} });
341     } else {
342         # biblio-level recall with no itemnumber. need to set itemnumber
343         $itemnumber = $params->{item}->itemnumber;
344         $self->update({ status => 'waiting', waiting_date => dt_from_string, expiration_date => $params->{expirationdate}, item_id => $itemnumber });
345     }
346
347     # send notice to recaller to pick up item
348     my $letter = C4::Letters::GetPreparedLetter(
349         module => 'circulation',
350         letter_code => 'PICKUP_RECALLED_ITEM',
351         branchcode => $self->pickup_library_id,
352         want_librarian => 0,
353         tables => {
354             biblio => $self->biblio_id,
355             borrowers => $self->patron_id,
356             items => $itemnumber,
357             recalls => $self->recall_id,
358         },
359     );
360
361     C4::Message->enqueue($letter, $self->patron->unblessed, 'email');
362
363     return $self;
364 }
365
366 =head3 revert_waiting
367
368     $recall->revert_waiting;
369
370 Revert recall waiting status.
371
372 =cut
373
374 sub revert_waiting {
375     my ( $self ) = @_;
376     if ( $self->item_level ){
377         $self->update({ status => 'requested', waiting_date => undef });
378     } else {
379         $self->update({ status => 'requested', waiting_date => undef, item_id => undef });
380     }
381     return $self;
382 }
383
384 =head3 should_be_overdue
385
386     if ( $recall->should_be_overdue ) {
387         $recall->set_overdue;
388     }
389
390 Return true if this recall should be marked overdue
391
392 =cut
393
394 sub should_be_overdue {
395     my ( $self ) = @_;
396     if ( $self->requested and $self->checkout and dt_from_string( $self->checkout->date_due ) <= dt_from_string ) {
397         return 1;
398     }
399     return 0;
400 }
401
402 =head3 set_overdue
403
404     $recall->set_overdue;
405
406 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.
407
408 =cut
409
410 sub set_overdue {
411     my ( $self, $params ) = @_;
412     my $interface = $params->{interface} || 'COMMANDLINE';
413     $self->update({ status => 'overdue' });
414     C4::Log::logaction( 'RECALLS', 'OVERDUE', $self->id, "Recall status set to overdue", $interface ) if ( C4::Context->preference('RecallsLog') );
415     return $self;
416 }
417
418 =head3 set_expired
419
420     $recall->set_expired({ interface => 'INTRANET' });
421
422 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.
423
424 =cut
425
426 sub set_expired {
427     my ( $self, $params ) = @_;
428     my $interface = $params->{interface} || 'COMMANDLINE';
429     $self->update({ status => 'expired', completed => 1, completed_date => dt_from_string });
430     C4::Log::logaction( 'RECALLS', 'EXPIRE', $self->id, "Recall expired", $interface ) if ( C4::Context->preference('RecallsLog') );
431     return $self;
432 }
433
434 =head3 set_cancelled
435
436     $recall->set_cancelled;
437
438 Set a recall as cancelled. This may be done manually, either by the borrower that placed the recall, or by the library.
439
440 =cut
441
442 sub set_cancelled {
443     my ( $self ) = @_;
444     $self->update({ status => 'cancelled', completed => 1, completed_date => dt_from_string });
445     C4::Log::logaction( 'RECALLS', 'CANCEL', $self->id, "Recall cancelled", 'INTRANET' ) if ( C4::Context->preference('RecallsLog') );
446     return $self;
447 }
448
449 =head3 set_fulfilled
450
451     $recall->set_fulfilled;
452
453 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.
454
455 =cut
456
457 sub set_fulfilled {
458     my ( $self ) = @_;
459     $self->update({ status => 'fulfilled', completed => 1, completed_date => dt_from_string });
460     C4::Log::logaction( 'RECALLS', 'FILL', $self->id, "Recall fulfilled", 'INTRANET' ) if ( C4::Context->preference('RecallsLog') );
461     return $self;
462 }
463
464 =head2 Internal methods
465
466 =head3 _type
467
468 =cut
469
470 sub _type {
471     return 'Recall';
472 }
473
474 1;