66578d83b014eb7807f11493ad951895f69e4f70
[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 item
61
62   my $item = Koha::StockRotationItem->item;
63
64 Returns the item associated with the current stock rotation item.
65
66 =cut
67
68 sub item {
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->item, $self->stage );
100     if ( $self->item->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->item->get_transfer;    # intransfer: don't advance.
121     return 1 if $self->fresh;                 # Just on rota: advance.
122     my $completed = $self->item->_result->branchtransfers->search(
123         { 'reason' => "StockrotationAdvance" },
124         { order_by => { -desc => 'datearrived' } }
125     );
126
127     # Do maths on whether we need to be moved on.
128     if ( $completed->count ) {
129         my $arrival  = dt_from_string( $completed->next->datearrived );
130         my $duration = $arrival->delta_days( dt_from_string() );
131         if ( $duration->in_units('days') >= $self->stage->duration ) {
132             return 1;
133         }
134         else {
135             return 0;
136         }
137     }
138     else {
139         warn "We have no historical branch transfer for item "
140           . $self->item->itemnumber
141           . "; This should not have happened!";
142     }
143 }
144
145 =head3 repatriate
146
147   1|0 = $sritem->repatriate
148
149 Put this item into branch transfer with 'StockrotationRepatriation' comment, so
150 that it may return to it's stage.branch to continue its rota as normal.
151
152 Note: Stockrotation falls outside of the normal branch transfer limits and so we
153 pass 'ignore_limits' in the call to request_transfer.
154
155 =cut
156
157 sub repatriate {
158     my ( $self, $msg ) = @_;
159
160     # Create the transfer.
161     my $transfer = try {
162         $self->item->request_transfer(
163             {
164                 to            => $self->stage->branchcode,
165                 reason        => "StockrotationRepatriation",
166                 comment       => $msg,
167                 ignore_limits => 1
168             }
169         );
170     };
171
172     # Ensure the homebranch is still in sync with the rota stage
173     $self->item->homebranch( $self->stage->branchcode_id )->store;
174
175     return defined($transfer) ? 1 : 0;
176 }
177
178 =head3 advance
179
180   1|0 = $sritem->advance;
181
182 Put this item into branch transfer with 'StockrotationAdvance' comment, to
183 transfer it to the next stage in its rota.
184
185 If this is the last stage in the rota and this rota is cyclical, we return to
186 the first stage.  If it is not cyclical, then we delete this
187 StockRotationItem.
188
189 If this item is 'indemand', and advance is invoked, we disable 'indemand' and
190 advance the item as per usual.
191
192 Note: Stockrotation falls outside of the normal branch transfer limits and so we
193 pass 'ignore_limits' in the call to request_transfer.
194
195 =cut
196
197 sub advance {
198     my ($self)         = @_;
199     my $item           = $self->item;
200     my $current_branch = $item->holdingbranch;
201     my $transfer;
202
203     # Find and interpret our stage
204     my $stage = $self->stage;
205     my $new_stage;
206     if ( $self->indemand && !$self->fresh ) {
207         $self->indemand(0);                                  # De-activate indemand
208         $new_stage = $stage;
209     }
210     else {
211         # New to rota?
212         if ( $self->fresh ) {
213             $new_stage = $self->stage->first_sibling || $self->stage;
214             $self->fresh(0);                                 # Reset fresh
215         }
216         # Last stage?
217         elsif ( !$stage->last_sibling ) {
218             # Cyclical rota?
219             if ( $stage->rota->cyclical ) {
220                 $new_stage =
221                   $stage->first_sibling || $stage;           # Revert to first stage.
222             }
223             else {
224                 $self->delete;                               # StockRotationItem is done.
225                 return 1;
226             }
227         }
228         else {
229             $new_stage = $self->stage->next_sibling;         # Just advance
230         }
231     }
232
233     # Update stage and record transfer
234     $self->stage_id( $new_stage->stage_id );                 # Set new stage
235     $self->store();
236     $item->homebranch( $new_stage->branchcode_id )->store;   # Update homebranch
237     $transfer = try {
238         $item->request_transfer(
239             {
240                 to            => $new_stage->branchcode,
241                 reason        => "StockrotationAdvance",
242                 ignore_limits => 1                      # Ignore transfer limits
243             }
244         );                                              # Add transfer
245     }
246     catch {
247         if ( $_->isa('Koha::Exceptions::Item::Transfer::InQueue') ) {
248             my $exception = $_;
249             my $found_transfer = $_->transfer;
250             if (   $found_transfer->in_transit
251                 || $found_transfer->reason eq 'Reserve'
252                 || $found_transfer->reason eq 'RotatingCollection' )
253             {
254                 return $item->request_transfer(
255                     {
256                         to            => $new_stage->branchcode,
257                         reason        => "StockrotationAdvance",
258                         ignore_limits => 1,
259                         enqueue       => 1
260                     }
261                 );                                      # Queue transfer
262             } else {
263                 return $item->request_transfer(
264                     {
265                         to            => $new_stage->branchcode,
266                         reason        => "StockrotationAdvance",
267                         ignore_limits => 1,
268                         replace       => 1
269                     }
270                 );                                      # Replace transfer
271             }
272         } else {
273             $_->rethrow();
274         }
275     };
276     $transfer->receive
277       if $item->holdingbranch eq $new_stage->branchcode_id && !$item->checkout;
278       # If item is already at branch, and not checked out
279       # If item is checked out, the return will either receive or initiate the transfer
280
281     return $transfer;
282 }
283
284 =head3 toggle_indemand
285
286   $sritem->toggle_indemand;
287
288 Toggle this items in_demand status.
289
290 If the item is in the process of being advanced to the next stage then we cancel
291 the transfer, revert the advancement and reset the 'StockrotationAdvance' counter,
292 as though 'in_demand' had been set prior to the call to advance, by updating the
293 in progress transfer.
294
295 =cut
296
297 sub toggle_indemand {
298     my ( $self ) = @_;
299
300     # Toggle the item's indemand flag
301     my $new_indemand = ($self->indemand == 1) ? 0 : 1;
302
303     # Cancel 'StockrotationAdvance' transfer if one is in progress
304     if ($new_indemand) {
305         my $item = $self->item;
306         my $transfer = $item->get_transfer;
307         if ($transfer && $transfer->reason eq 'StockrotationAdvance') {
308             my $stage = $self->stage;
309             my $new_stage;
310             if ( $stage->rota->cyclical && !$stage->first_sibling ) { # First stage
311                 $new_stage = $stage->last_sibling;
312             } else {
313                 $new_stage = $stage->previous_sibling;
314             }
315             $self->stage_id($new_stage->stage_id)->store;        # Revert stage change
316             $item->homebranch($new_stage->branchcode_id)->store; # Revert update homebranch
317             $new_indemand = 0;                                   # Reset indemand
318             $transfer->tobranch($new_stage->branchcode_id);      # Reset StockrotationAdvance
319             $transfer->datearrived(dt_from_string);              # Reset StockrotationAdvance
320             $transfer->store;
321         }
322     }
323
324     $self->indemand($new_indemand)->store;
325 }
326
327 =head3 investigate
328
329   my $report = $item->investigate;
330
331 Return the base set of information, namely this individual item's report, for
332 generating stockrotation reports about this stockrotationitem.
333
334 =cut
335
336 sub investigate {
337     my ( $self ) = @_;
338     my $item_report = {
339         title      => $self->item->_result->biblioitem
340             ->biblionumber->title,
341         author     => $self->item->_result->biblioitem
342             ->biblionumber->author,
343         callnumber => $self->item->itemcallnumber,
344         location   => $self->item->location,
345         onloan     => $self->item->onloan,
346         barcode    => $self->item->barcode,
347         itemnumber => $self->itemnumber_id,
348         branch => $self->item->_result->holdingbranch,
349         object => $self,
350     };
351     my $reason;
352     if ( $self->fresh ) {
353         $reason = 'initiation';
354     } elsif ( $self->needs_repatriating ) {
355         $reason = 'repatriation';
356     } elsif ( $self->needs_advancing ) {
357         $reason = 'advancement';
358         $reason = 'in-demand' if $self->indemand;
359     } else {
360         $reason = 'not-ready';
361     }
362     $item_report->{reason} = $reason;
363
364     return $item_report;
365 }
366
367 1;
368
369 =head1 AUTHOR
370
371 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
372
373 =cut