Bug 24446: (follow-up) Handle cases of pre-existing transfers
[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;
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)->store;                          # 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)->store;                         # 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 )->store;          # Set new stage
233     $item->homebranch( $new_stage->branchcode_id )->store;   # Update homebranch
234     $transfer = try {
235         $item->request_transfer(
236             {
237                 to            => $new_stage->branchcode,
238                 reason        => "StockrotationAdvance",
239                 ignore_limits => 1                      # Ignore transfer limits
240             }
241         );                                              # Add transfer
242     }
243     catch {
244         if ( $_->isa('Koha::Exceptions::Item::Transfer::Found') ) {
245             my $exception = $_;
246             my $found_transfer = $_->transfer;
247             if (   $found_transfer->in_transit
248                 || $found_transfer->reason eq 'Reserve' )
249             {
250                 return $item->request_transfer(
251                     {
252                         to            => $new_stage->branchcode,
253                         reason        => "StockrotationAdvance",
254                         ignore_limits => 1,
255                         enqueue       => 1
256                     }
257                 );                                      # Queue transfer
258             } else {
259                 return $item->request_transfer(
260                     {
261                         to            => $new_stage->branchcode,
262                         reason        => "StockrotationAdvance",
263                         ignore_limits => 1,
264                         replace       => 1
265                     }
266                 );                                      # Replace transfer
267             }
268         } else {
269             $_->rethrow();
270         }
271     };
272     $transfer->receive
273       if $item->holdingbranch eq $new_stage->branchcode_id;  # Already at branch
274
275     return $transfer;
276 }
277
278 =head3 investigate
279
280   my $report = $item->investigate;
281
282 Return the base set of information, namely this individual item's report, for
283 generating stockrotation reports about this stockrotationitem.
284
285 =cut
286
287 sub investigate {
288     my ( $self ) = @_;
289     my $item_report = {
290         title      => $self->itemnumber->_result->biblioitem
291             ->biblionumber->title,
292         author     => $self->itemnumber->_result->biblioitem
293             ->biblionumber->author,
294         callnumber => $self->itemnumber->itemcallnumber,
295         location   => $self->itemnumber->location,
296         onloan     => $self->itemnumber->onloan,
297         barcode    => $self->itemnumber->barcode,
298         itemnumber => $self->itemnumber_id,
299         branch => $self->itemnumber->_result->holdingbranch,
300         object => $self,
301     };
302     my $reason;
303     if ( $self->fresh ) {
304         $reason = 'initiation';
305     } elsif ( $self->needs_repatriating ) {
306         $reason = 'repatriation';
307     } elsif ( $self->needs_advancing ) {
308         $reason = 'advancement';
309         $reason = 'in-demand' if $self->indemand;
310     } else {
311         $reason = 'not-ready';
312     }
313     $item_report->{reason} = $reason;
314
315     return $item_report;
316 }
317
318 1;
319
320 =head1 AUTHOR
321
322 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
323
324 =cut