Bug 29407: Make the pickup locations dropdown JS reusable
[koha.git] / Koha / StockRotationItem.pm
1 package Koha::StockRotationItem;
2
3 # Copyright PTFS Europe 2016
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 DateTime;
23 use DateTime::Duration;
24 use Koha::Database;
25 use Koha::DateUtils qw( dt_from_string );
26 use Koha::Item::Transfer;
27 use Koha::Item;
28 use Koha::StockRotationStage;
29 use Try::Tiny qw( catch try );
30
31 use base qw(Koha::Object);
32
33 =head1 NAME
34
35 StockRotationItem - Koha StockRotationItem Object class
36
37 =head1 SYNOPSIS
38
39 StockRotationItem class used primarily by stockrotation .pls and the stock
40 rotation cron script.
41
42 =head1 DESCRIPTION
43
44 Standard Koha::Objects definitions, and additional methods.
45
46 =head1 API
47
48 =head2 Class Methods
49
50 =cut
51
52 =head3 _type
53
54 =cut
55
56 sub _type {
57     return 'Stockrotationitem';
58 }
59
60 =head3 itemnumber
61
62   my $item = Koha::StockRotationItem->itemnumber;
63
64 Returns the item associated with the current stock rotation item.
65
66 =cut
67
68 sub itemnumber {
69     my ( $self ) = @_;
70     my $rs = $self->_result->itemnumber;
71     return Koha::Item->_new_from_dbic( $rs );
72 }
73
74 =head3 stage
75
76   my $stage = Koha::StockRotationItem->stage;
77
78 Returns the stage associated with the current stock rotation item.
79
80 =cut
81
82 sub stage {
83     my ( $self ) = @_;
84     my $rs = $self->_result->stage;
85     return Koha::StockRotationStage->_new_from_dbic( $rs );
86 }
87
88 =head3 needs_repatriating
89
90   1|0 = $item->needs_repatriating;
91
92 Return 1 if this item is currently not at the library it should be at
93 according to our stockrotation plan.
94
95 =cut
96
97 sub needs_repatriating {
98     my ( $self ) = @_;
99     my ( $item, $stage ) = ( $self->itemnumber, $self->stage );
100     if ( $self->itemnumber->get_transfer ) {
101         return 0;               # We're in transit.
102     } elsif ( $item->holdingbranch ne $stage->branchcode_id
103                   || $item->homebranch ne $stage->branchcode_id ) {
104         return 1;               # We're not where we should be.
105     } else {
106         return 0;               # We're at home.
107     }
108 }
109
110 =head3 needs_advancing
111
112   1|0 = $item->needs_advancing;
113
114 Return 1 if this item is ready to be moved on to the next stage in its rota.
115
116 =cut
117
118 sub needs_advancing {
119     my ( $self ) = @_;
120     return 0 if $self->itemnumber->get_transfer; # intransfer: don't advance.
121     return 1 if $self->fresh;                    # Just on rota: advance.
122     my $completed = $self->itemnumber->_result->branchtransfers->search(
123         { 'reason' => "StockrotationAdvance" },
124         { order_by => { -desc => 'datearrived' } }
125     );
126     # Do maths on whether we need to be moved on.
127     if ( $completed->count ) {
128         my $arrival = dt_from_string(
129             $completed->next->datearrived, 'iso'
130         );
131         my $duration = DateTime::Duration
132             ->new( days => $self->stage->duration );
133         if ( $arrival + $duration le dt_from_string() ) {
134             return 1;
135         } else {
136             return 0;
137         }
138     } else {
139         warn "We have no historical branch transfer for itemnumber " . $self->itemnumber->itemnumber . "; This should not have happened!";
140     }
141 }
142
143 =head3 repatriate
144
145   1|0 = $sritem->repatriate
146
147 Put this item into branch transfer with 'StockrotationRepatriation' comment, so
148 that it may return to it's stage.branch to continue its rota as normal.
149
150 Note: Stockrotation falls outside of the normal branch transfer limits and so we
151 pass 'ignore_limits' in the call to request_transfer.
152
153 =cut
154
155 sub repatriate {
156     my ( $self, $msg ) = @_;
157
158     # Create the transfer.
159     my $transfer = try {
160         $self->itemnumber->request_transfer(
161             {
162                 to            => $self->stage->branchcode,
163                 reason        => "StockrotationRepatriation",
164                 comment       => $msg,
165                 ignore_limits => 1
166             }
167         );
168     };
169
170     # Ensure the homebranch is still in sync with the rota stage
171     $self->itemnumber->homebranch( $self->stage->branchcode_id )->store;
172
173     return defined($transfer) ? 1 : 0;
174 }
175
176 =head3 advance
177
178   1|0 = $sritem->advance;
179
180 Put this item into branch transfer with 'StockrotationAdvance' comment, to
181 transfer it to the next stage in its rota.
182
183 If this is the last stage in the rota and this rota is cyclical, we return to
184 the first stage.  If it is not cyclical, then we delete this
185 StockRotationItem.
186
187 If this item is 'indemand', and advance is invoked, we disable 'indemand' and
188 advance the item as per usual.
189
190 Note: Stockrotation falls outside of the normal branch transfer limits and so we
191 pass 'ignore_limits' in the call to request_transfer.
192
193 =cut
194
195 sub advance {
196     my ($self)         = @_;
197     my $item           = $self->itemnumber;
198     my $current_branch = $item->holdingbranch;
199     my $transfer;
200
201     # Find and interpret our stage
202     my $stage = $self->stage;
203     my $new_stage;
204     if ( $self->indemand && !$self->fresh ) {
205         $self->indemand(0);                                  # De-activate indemand
206         $new_stage = $stage;
207     }
208     else {
209         # New to rota?
210         if ( $self->fresh ) {
211             $new_stage = $self->stage->first_sibling || $self->stage;
212             $self->fresh(0);                                 # Reset fresh
213         }
214         # Last stage?
215         elsif ( !$stage->last_sibling ) {
216             # Cyclical rota?
217             if ( $stage->rota->cyclical ) {
218                 $new_stage =
219                   $stage->first_sibling || $stage;           # Revert to first stage.
220             }
221             else {
222                 $self->delete;                               # StockRotationItem is done.
223                 return 1;
224             }
225         }
226         else {
227             $new_stage = $self->stage->next_sibling;         # Just advance
228         }
229     }
230
231     # Update stage and record transfer
232     $self->stage_id( $new_stage->stage_id );                 # Set new stage
233     $self->store();
234     $item->homebranch( $new_stage->branchcode_id )->store;   # Update homebranch
235     $transfer = try {
236         $item->request_transfer(
237             {
238                 to            => $new_stage->branchcode,
239                 reason        => "StockrotationAdvance",
240                 ignore_limits => 1                      # Ignore transfer limits
241             }
242         );                                              # Add transfer
243     }
244     catch {
245         if ( $_->isa('Koha::Exceptions::Item::Transfer::InQueue') ) {
246             my $exception = $_;
247             my $found_transfer = $_->transfer;
248             if (   $found_transfer->in_transit
249                 || $found_transfer->reason eq 'Reserve'
250                 || $found_transfer->reason eq 'RotatingCollection' )
251             {
252                 return $item->request_transfer(
253                     {
254                         to            => $new_stage->branchcode,
255                         reason        => "StockrotationAdvance",
256                         ignore_limits => 1,
257                         enqueue       => 1
258                     }
259                 );                                      # Queue transfer
260             } else {
261                 return $item->request_transfer(
262                     {
263                         to            => $new_stage->branchcode,
264                         reason        => "StockrotationAdvance",
265                         ignore_limits => 1,
266                         replace       => 1
267                     }
268                 );                                      # Replace transfer
269             }
270         } else {
271             $_->rethrow();
272         }
273     };
274     $transfer->receive
275       if $item->holdingbranch eq $new_stage->branchcode_id;  # Already at branch
276
277     return $transfer;
278 }
279
280 =head3 toggle_indemand
281
282   $sritem->toggle_indemand;
283
284 Toggle this items in_demand status.
285
286 If the item is in the process of being advanced to the next stage then we cancel
287 the transfer, revert the advancement and reset the 'StockrotationAdvance' counter,
288 as though 'in_demand' had been set prior to the call to advance, by updating the
289 in progress transfer.
290
291 =cut
292
293 sub toggle_indemand {
294     my ( $self ) = @_;
295
296     # Toggle the item's indemand flag
297     my $new_indemand = ($self->indemand == 1) ? 0 : 1;
298
299     # Cancel 'StockrotationAdvance' transfer if one is in progress
300     if ($new_indemand) {
301         my $item = $self->itemnumber;
302         my $transfer = $item->get_transfer;
303         if ($transfer && $transfer->reason eq 'StockrotationAdvance') {
304             my $stage = $self->stage;
305             my $new_stage;
306             if ( $stage->rota->cyclical && !$stage->first_sibling ) { # First stage
307                 $new_stage = $stage->last_sibling;
308             } else {
309                 $new_stage = $stage->previous_sibling;
310             }
311             $self->stage_id($new_stage->stage_id)->store;        # Revert stage change
312             $item->homebranch($new_stage->branchcode_id)->store; # Revert update homebranch
313             $new_indemand = 0;                                   # Reset indemand
314             $transfer->tobranch($new_stage->branchcode_id);      # Reset StockrotationAdvance
315             $transfer->datearrived(dt_from_string);              # Reset StockrotationAdvance
316             $transfer->store;
317         }
318     }
319
320     $self->indemand($new_indemand)->store;
321 }
322
323 =head3 investigate
324
325   my $report = $item->investigate;
326
327 Return the base set of information, namely this individual item's report, for
328 generating stockrotation reports about this stockrotationitem.
329
330 =cut
331
332 sub investigate {
333     my ( $self ) = @_;
334     my $item_report = {
335         title      => $self->itemnumber->_result->biblioitem
336             ->biblionumber->title,
337         author     => $self->itemnumber->_result->biblioitem
338             ->biblionumber->author,
339         callnumber => $self->itemnumber->itemcallnumber,
340         location   => $self->itemnumber->location,
341         onloan     => $self->itemnumber->onloan,
342         barcode    => $self->itemnumber->barcode,
343         itemnumber => $self->itemnumber_id,
344         branch => $self->itemnumber->_result->holdingbranch,
345         object => $self,
346     };
347     my $reason;
348     if ( $self->fresh ) {
349         $reason = 'initiation';
350     } elsif ( $self->needs_repatriating ) {
351         $reason = 'repatriation';
352     } elsif ( $self->needs_advancing ) {
353         $reason = 'advancement';
354         $reason = 'in-demand' if $self->indemand;
355     } else {
356         $reason = 'not-ready';
357     }
358     $item_report->{reason} = $reason;
359
360     return $item_report;
361 }
362
363 1;
364
365 =head1 AUTHOR
366
367 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
368
369 =cut