Bug 32942: (QA follow-up) Moving Suggestion->STATUS check to Suggestion::store
[koha.git] / Koha / Recalls.pm
1 package Koha::Recalls;
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::Recall;
24 use Koha::DateUtils qw( dt_from_string );
25 use Koha::Plugins;
26
27 use C4::Stats qw( UpdateStats );
28
29 use base qw(Koha::Objects);
30
31 =head1 NAME
32
33 Koha::Recalls - Koha Recalls Object set class
34
35 =head1 API
36
37 =head2 Class methods
38
39 =head3 filter_by_current
40
41     my $current_recalls = $recalls->filter_by_current;
42
43 Returns a new resultset, filtering out finished recalls.
44
45 =cut
46
47 sub filter_by_current {
48     my ($self) = @_;
49
50     return $self->search(
51         {
52             status => [
53                 'in_transit',
54                 'overdue',
55                 'requested',
56                 'waiting',
57             ]
58         }
59     );
60 }
61
62 =head3 filter_by_finished
63
64     my $finished_recalls = $recalls->filter_by_finished;
65
66 Returns a new resultset, filtering out current recalls.
67
68 =cut
69
70 sub filter_by_finished {
71     my ($self) = @_;
72
73     return $self->search(
74         {
75             status => [
76                 'cancelled',
77                 'expired',
78                 'fulfilled',
79             ]
80         }
81     );
82 }
83
84 =head3 add_recall
85
86     my ( $recall, $due_interval, $due_date ) = Koha::Recalls->add_recall({
87         patron => $patron_object,
88         biblio => $biblio_object,
89         branchcode => $branchcode,
90         item => $item_object,
91         expirationdate => $expirationdate,
92         interface => 'OPAC',
93     });
94
95 Add a new requested recall. We assume at this point that a recall is allowed to be placed on this item or biblio. We are past the checks and are now doing the recall.
96 Interface param is either OPAC or INTRANET
97 Send a RETURN_RECALLED_ITEM notice.
98 Add statistics and logs.
99 #FIXME: Add recallnotes and priority when staff-side recalls is added
100
101 =cut
102
103 sub add_recall {
104     my ( $self, $params ) = @_;
105
106     my $patron = $params->{patron};
107     my $biblio = $params->{biblio};
108     return if ( !defined($patron) or !defined($biblio) );
109     my $branchcode = $params->{branchcode};
110     $branchcode ||= $patron->branchcode;
111     my $item = $params->{item};
112     my $itemnumber = $item ? $item->itemnumber : undef;
113     my $expirationdate = $params->{expirationdate};
114     my $interface = $params->{interface};
115
116     if ( $expirationdate ){
117         my $now = dt_from_string;
118         $expirationdate = dt_from_string($expirationdate)->set({ hour => $now->hour, minute => $now->minute, second => $now->second });
119     }
120
121     my $recall_request = Koha::Recall->new({
122         patron_id => $patron->borrowernumber,
123         created_date => dt_from_string(),
124         biblio_id => $biblio->biblionumber,
125         pickup_library_id => $branchcode,
126         status => 'requested',
127         item_id => defined $itemnumber ? $itemnumber : undef,
128         expiration_date => $expirationdate,
129         item_level => defined $itemnumber ? 1 : 0,
130     })->store;
131
132     if (defined $recall_request->id){ # successful recall
133         my $recall = Koha::Recalls->find( $recall_request->id );
134
135         # get checkout and adjust due date based on circulation rules
136         my $checkout = $recall->checkout;
137         my $recall_due_date_interval = Koha::CirculationRules->get_effective_rule({
138             categorycode => $checkout->patron->categorycode,
139             itemtype => $checkout->item->effective_itemtype,
140             branchcode => $branchcode,
141             rule_name => 'recall_due_date_interval',
142         });
143         my $due_interval = defined $recall_due_date_interval ? $recall_due_date_interval->rule_value : 5;
144         my $timestamp = dt_from_string( $recall->timestamp );
145         my $checkout_timestamp = dt_from_string( $checkout->date_due );
146         my $due_date = $timestamp->set(
147             {
148                 hour   => $checkout_timestamp->hour, minute => $checkout_timestamp->minute,
149                 second => $checkout_timestamp->second
150             }
151         )->add( days => $due_interval );
152         $checkout->update( { date_due => $due_date } );
153
154         # get itemnumber of most relevant checkout if a biblio-level recall
155         unless ( $recall->item_level ) { $itemnumber = $checkout->itemnumber; }
156
157         # send notice to user with recalled item checked out
158         my $letter = C4::Letters::GetPreparedLetter (
159             module => 'circulation',
160             letter_code => 'RETURN_RECALLED_ITEM',
161             branchcode => $recall->pickup_library_id,
162             tables => {
163                 biblio => $biblio->biblionumber,
164                 borrowers => $checkout->borrowernumber,
165                 items => $itemnumber,
166                 issues => $itemnumber,
167             },
168         );
169
170         C4::Message->enqueue( $letter, $checkout->patron, 'email' );
171
172         $item = Koha::Items->find( $itemnumber );
173         # add to statistics table
174         C4::Stats::UpdateStats(
175             {
176                 branch         => C4::Context->userenv->{'branch'},
177                 type           => 'recall',
178                 itemnumber     => $itemnumber,
179                 borrowernumber => $recall->patron_id,
180                 itemtype       => $item->effective_itemtype,
181                 ccode          => $item->ccode,
182                 categorycode   => $checkout->patron->categorycode
183             }
184         );
185
186         Koha::Plugins->call(
187             'after_recall_action',
188             {
189                 action  => 'add',
190                 payload => { recall => $recall->get_from_storage }, # FIXME Bug 32107
191             }
192         );
193
194         # add action log
195         C4::Log::logaction( 'RECALLS', 'CREATE', $recall->id, "Recall requested by borrower #" . $recall->patron_id, $interface ) if ( C4::Context->preference('RecallsLog') );
196
197         return ( $recall, $due_interval, $due_date );
198     }
199
200     # unable to add recall
201     return;
202 }
203
204 =head3 move_recall
205
206     my $message = Koha::Recalls->move_recall({
207         recall_id = $recall_id,
208         action => $action,
209         item => $item_object,
210         borrowernumber => $borrowernumber,
211     });
212
213 A patron is attempting to check out an item that has been recalled by another patron.
214 If the recall is requested/overdue, they have the option of cancelling the recall.
215 If the recall is waiting, they also have the option of reverting the waiting status.
216
217 We can also fulfill the recall here if the recall is placed by this borrower.
218
219 recall_id = ID of the recall to perform the action on
220 action = either cancel or revert
221 item = item object that the patron is attempting to check out
222 borrowernumber = borrowernumber of the patron that is attemptig to check out
223
224 =cut
225
226 sub move_recall {
227     my ( $self, $params ) = @_;
228
229     my $recall_id = $params->{recall_id};
230     my $action = $params->{action};
231     return 'no recall_id provided' if ( !defined $recall_id );
232     my $item = $params->{item};
233     my $borrowernumber = $params->{borrowernumber};
234
235     my $message = 'no action provided';
236
237     if ( $action and $action eq 'cancel' ) {
238         my $recall = Koha::Recalls->find( $recall_id );
239         $recall->set_cancelled;
240         $message = 'cancelled';
241     } elsif ( $action and $action eq 'revert' ) {
242         my $recall = Koha::Recalls->find( $recall_id );
243         $recall->revert_waiting;
244         $message = 'reverted';
245     }
246
247     if ( $message eq 'no action provided' and $item and $item->biblionumber and $borrowernumber ) {
248         # move_recall was not called to revert or cancel, but was called to fulfill
249         my $recall = Koha::Recalls->search(
250             {
251                 patron_id => $borrowernumber,
252                 biblio_id => $item->biblionumber,
253                 item_id   => [ $item->itemnumber, undef ],
254                 completed => 0,
255             },
256             { order_by => { -asc => 'created_date' } }
257         )->next;
258         if ( $recall ) {
259             $recall->set_fulfilled;
260             $message = 'fulfilled';
261         }
262     }
263
264     return $message;
265 }
266
267 =head2 Internal methods
268
269 =head3 _type
270
271 =cut
272
273 sub _type {
274     return 'Recall';
275 }
276
277 =head3 object_class
278
279 =cut
280
281 sub object_class {
282     return 'Koha::Recall';
283 }
284
285 1;