View | Details | Raw Unified | Return to bug 11897
Collapse All | Expand All

(-)a/Koha/Item.pm (-1 / +48 lines)
Lines 31-36 use Koha::Item::Transfer::Limits; Link Here
31
use Koha::Item::Transfers;
31
use Koha::Item::Transfers;
32
use Koha::Patrons;
32
use Koha::Patrons;
33
use Koha::Libraries;
33
use Koha::Libraries;
34
use Koha::StockRotationItem;
35
use Koha::StockRotationRotas;
34
36
35
use base qw(Koha::Object);
37
use base qw(Koha::Object);
36
38
Lines 282-288 sub current_holds { Link Here
282
    return Koha::Holds->_new_from_dbic($hold_rs);
284
    return Koha::Holds->_new_from_dbic($hold_rs);
283
}
285
}
284
286
285
=head3 type
287
=head3 stockrotationitem
288
289
  my $sritem = Koha::Item->stockrotationitem;
290
291
Returns the stock rotation item associated with the current item.
292
293
=cut
294
295
sub stockrotationitem {
296
    my ( $self ) = @_;
297
    my $rs = $self->_result->stockrotationitem;
298
    return 0 if !$rs;
299
    return Koha::StockRotationItem->_new_from_dbic( $rs );
300
}
301
302
=head3 add_to_rota
303
304
  my $item = $item->add_to_rota($rota_id);
305
306
Add this item to the rota identified by $ROTA_ID, which means associating it
307
with the first stage of that rota.  Should this item already be associated
308
with a rota, then we will move it to the new rota.
309
310
=cut
311
312
sub add_to_rota {
313
    my ( $self, $rota_id ) = @_;
314
    Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
315
    return $self;
316
}
317
318
=head3 biblio
319
320
  my $biblio = $item->biblio;
321
322
Returns the biblio associated with the current item.
323
324
=cut
325
326
sub biblio {
327
    my ( $self ) = @_;
328
    my $rs = $self->_result->biblio;
329
    return Koha::Biblio->_new_from_dbic( $rs );
330
}
331
332
=head3 _type
286
333
287
=cut
334
=cut
288
335
(-)a/Koha/Library.pm (+46 lines)
Lines 24-29 use Carp; Link Here
24
use C4::Context;
24
use C4::Context;
25
25
26
use Koha::Database;
26
use Koha::Database;
27
use Koha::StockRotationStages;
27
28
28
use base qw(Koha::Object);
29
use base qw(Koha::Object);
29
30
Lines 41-46 TODO: Ask the author to add a proper description Link Here
41
42
42
=cut
43
=cut
43
44
45
sub get_categories {
46
    my ( $self, $params ) = @_;
47
    # TODO This should return Koha::LibraryCategories
48
    return $self->{_result}->categorycodes( $params );
49
}
50
51
=head3 update_categories
52
53
TODO: Ask the author to add a proper description
54
55
=cut
56
57
sub update_categories {
58
    my ( $self, $categories ) = @_;
59
    $self->_result->delete_related( 'branchrelations' );
60
    $self->add_to_categories( $categories );
61
}
62
63
=head3 add_to_categories
64
65
TODO: Ask the author to add a proper description
66
67
=cut
68
69
sub add_to_categories {
70
    my ( $self, $categories ) = @_;
71
    for my $category ( @$categories ) {
72
        $self->_result->add_to_categorycodes( $category->_result );
73
    }
74
}
75
76
=head3 stockrotationstages
77
78
  my $stages = Koha::Library->stockrotationstages;
79
80
Returns the stockrotation stages associated with this Library.
81
82
=cut
83
84
sub stockrotationstages {
85
    my ( $self ) = @_;
86
    my $rs = $self->_result->stockrotationstages;
87
    return Koha::StockRotationStages->_new_from_dbic( $rs );
88
}
89
44
=head3 get_effective_marcorgcode
90
=head3 get_effective_marcorgcode
45
91
46
    my $marcorgcode = Koha::Libraries->find( $library_id )->get_effective_marcorgcode();
92
    my $marcorgcode = Koha::Libraries->find( $library_id )->get_effective_marcorgcode();
(-)a/Koha/REST/V1/Stage.pm (+60 lines)
Line 0 Link Here
1
package Koha::REST::V1::Stage;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
6
# terms of the GNU General Public License as published by the Free Software
7
# Foundation; either version 3 of the License, or (at your option) any later
8
# version.
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License along
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18
use Modern::Perl;
19
20
use Mojo::Base 'Mojolicious::Controller';
21
22
use Koha::StockRotationRotas;
23
use Koha::StockRotationStages;
24
25
=head1 NAME
26
27
Koha::REST::V1::Stage
28
29
=head2 Operations
30
31
=head3 move
32
33
Move a stage up or down the stockrotation rota.
34
35
=cut
36
37
sub move {
38
    my $c = shift->openapi->valid_input or return;
39
    my $input = $c->validation->output;
40
41
    my $rota  = Koha::StockRotationRotas->find( $input->{rota_id} );
42
    my $stage = Koha::StockRotationStages->find( $input->{stage_id} );
43
44
    if ( $stage && $rota ) {
45
        my $result = $stage->move_to( $input->{position} );
46
        return $c->render( openapi => {}, status => 200 ) if $result;
47
        return $c->render(
48
            openapi => { error => "Bad request - new position invalid" },
49
            status  => 400
50
        );
51
    }
52
    else {
53
        return $c->render(
54
            openapi => { error => "Not found - Invalid rota or stage ID" },
55
            status  => 404
56
        );
57
    }
58
}
59
60
1;
(-)a/Koha/StockRotationItem.pm (+273 lines)
Line 0 Link Here
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 under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
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
30
use base qw(Koha::Object);
31
32
=head1 NAME
33
34
StockRotationItem - Koha StockRotationItem Object class
35
36
=head1 SYNOPSIS
37
38
StockRotationItem class used primarily by stockrotation .pls and the stock
39
rotation cron script.
40
41
=head1 DESCRIPTION
42
43
Standard Koha::Objects definitions, and additional methods.
44
45
=head1 API
46
47
=head2 Class Methods
48
49
=cut
50
51
=head3 _type
52
53
=cut
54
55
sub _type {
56
    return 'Stockrotationitem';
57
}
58
59
=head3 itemnumber
60
61
  my $item = Koha::StockRotationItem->itemnumber;
62
63
Returns the item associated with the current stock rotation item.
64
65
=cut
66
67
sub itemnumber {
68
    my ( $self ) = @_;
69
    my $rs = $self->_result->itemnumber;
70
    return Koha::Item->_new_from_dbic( $rs );
71
}
72
73
=head3 stage
74
75
  my $stage = Koha::StockRotationItem->stage;
76
77
Returns the stage associated with the current stock rotation item.
78
79
=cut
80
81
sub stage {
82
    my ( $self ) = @_;
83
    my $rs = $self->_result->stage;
84
    return Koha::StockRotationStage->_new_from_dbic( $rs );
85
}
86
87
=head3 needs_repatriating
88
89
  1|0 = $item->needs_repatriating;
90
91
Return 1 if this item is currently not at the library it should be at
92
according to our stockrotation plan.
93
94
=cut
95
96
sub needs_repatriating {
97
    my ( $self ) = @_;
98
    my ( $item, $stage ) = ( $self->itemnumber, $self->stage );
99
    if ( $self->itemnumber->get_transfer ) {
100
        return 0;               # We're in transit.
101
    } elsif ( $item->holdingbranch ne $stage->branchcode_id
102
                  || $item->homebranch ne $stage->branchcode_id ) {
103
        return 1;               # We're not where we should be.
104
    } else {
105
        return 0;               # We're at home.
106
    }
107
}
108
109
=head3 needs_advancing
110
111
  1|0 = $item->needs_advancing;
112
113
Return 1 if this item is ready to be moved on to the next stage in its rota.
114
115
=cut
116
117
sub needs_advancing {
118
    my ( $self ) = @_;
119
    return 0 if $self->itemnumber->get_transfer; # intransfer: don't advance.
120
    return 1 if $self->fresh;                    # Just on rota: advance.
121
    my $completed = $self->itemnumber->_result->branchtransfers->search(
122
        { 'comments'    => "StockrotationAdvance" },
123
        { order_by => { -desc => 'datearrived' } }
124
    );
125
    # Do maths on whether we need to be moved on.
126
    if ( $completed->count ) {
127
        my $arrival = dt_from_string(
128
            $completed->next->datearrived, 'iso'
129
        );
130
        my $duration = DateTime::Duration
131
            ->new( days => $self->stage->duration );
132
        if ( $arrival + $duration le DateTime->now ) {
133
            return 1;
134
        } else {
135
            return 0;
136
        }
137
    } else {
138
        die "We have no historical branch transfer; this should not have happened!";
139
    }
140
}
141
142
=head3 repatriate
143
144
  1|0 = $sritem->repatriate
145
146
Put this item into branch transfer with 'StockrotationCorrection' comment, so
147
that it may return to it's stage.branch to continue its rota as normal.
148
149
=cut
150
151
sub repatriate {
152
    my ( $self, $msg ) = @_;
153
    # Create the transfer.
154
    my $transfer_stored = Koha::Item::Transfer->new({
155
        'itemnumber' => $self->itemnumber_id,
156
        'frombranch' => $self->itemnumber->holdingbranch,
157
        'tobranch'   => $self->stage->branchcode_id,
158
        'datesent'   => DateTime->now,
159
        'comments'   => $msg || "StockrotationRepatriation",
160
    })->store;
161
    $self->itemnumber->homebranch($self->stage->branchcode_id)->store;
162
    return $transfer_stored;
163
}
164
165
=head3 advance
166
167
  1|0 = $sritem->advance;
168
169
Put this item into branch transfer with 'StockrotationAdvance' comment, to
170
transfer it to the next stage in its rota.
171
172
If this is the last stage in the rota and this rota is cyclical, we return to
173
the first stage.  If it is not cyclical, then we delete this
174
StockRotationItem.
175
176
If this item is 'indemand', and advance is invoked, we disable 'indemand' and
177
advance the item as per usual.
178
179
=cut
180
181
sub advance {
182
    my ( $self ) = @_;
183
    my $item = $self->itemnumber;
184
    my $transfer = Koha::Item::Transfer->new({
185
        'itemnumber' => $self->itemnumber_id,
186
        'frombranch' => $item->holdingbranch,
187
        'datesent'   => DateTime->now,
188
        'comments'   => "StockrotationAdvance"
189
    });
190
191
    if ( $self->indemand && !$self->fresh ) {
192
        $self->indemand(0)->store;  # De-activate indemand
193
        $transfer->tobranch($self->stage->branchcode_id);
194
        $transfer->datearrived(DateTime->now);
195
    } else {
196
        # Find and update our stage.
197
        my $stage = $self->stage;
198
        my $new_stage;
199
        if ( $self->fresh ) {   # Just added to rota
200
            $new_stage = $self->stage->first_sibling || $self->stage;
201
            $transfer->tobranch($new_stage->branchcode_id);
202
            $transfer->datearrived(DateTime->now) # Already at first branch
203
                if $item->holdingbranch eq $new_stage->branchcode_id;
204
            $self->fresh(0)->store;         # Reset fresh
205
        } elsif ( !$stage->last_sibling ) { # Last stage
206
            if ( $stage->rota->cyclical ) { # Cyclical rota?
207
                # Revert to first stage.
208
                $new_stage = $stage->first_sibling || $stage;
209
                $transfer->tobranch($new_stage->branchcode_id);
210
                $transfer->datearrived(DateTime->now);
211
            } else {
212
                $self->delete;  # StockRotationItem is done.
213
                return 1;
214
            }
215
        } else {
216
            # Just advance.
217
            $new_stage = $self->stage->next_sibling;
218
        }
219
        $self->stage_id($new_stage->stage_id)->store;        # Set new stage
220
        $item->homebranch($new_stage->branchcode_id)->store; # Update homebranch
221
        $transfer->tobranch($new_stage->branchcode_id);      # Send to new branch
222
    }
223
224
    return $transfer->store;
225
}
226
227
=head3 investigate
228
229
  my $report = $item->investigate;
230
231
Return the base set of information, namely this individual item's report, for
232
generating stockrotation reports about this stockrotationitem.
233
234
=cut
235
236
sub investigate {
237
    my ( $self ) = @_;
238
    my $item_report = {
239
        title      => $self->itemnumber->_result->biblioitem
240
            ->biblionumber->title,
241
        author     => $self->itemnumber->_result->biblioitem
242
            ->biblionumber->author,
243
        callnumber => $self->itemnumber->itemcallnumber,
244
        location   => $self->itemnumber->location,
245
        onloan     => $self->itemnumber->onloan,
246
        barcode    => $self->itemnumber->barcode,
247
        itemnumber => $self->itemnumber_id,
248
        branch => $self->itemnumber->_result->holdingbranch,
249
        object => $self,
250
    };
251
    my $reason;
252
    if ( $self->fresh ) {
253
        $reason = 'initiation';
254
    } elsif ( $self->needs_repatriating ) {
255
        $reason = 'repatriation';
256
    } elsif ( $self->needs_advancing ) {
257
        $reason = 'advancement';
258
        $reason = 'in-demand' if $self->indemand;
259
    } else {
260
        $reason = 'not-ready';
261
    }
262
    $item_report->{reason} = $reason;
263
264
    return $item_report;
265
}
266
267
1;
268
269
=head1 AUTHOR
270
271
Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
272
273
=cut
(-)a/Koha/StockRotationItems.pm (+128 lines)
Line 0 Link Here
1
package Koha::StockRotationItems;
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 under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
use Modern::Perl;
21
22
use Koha::Database;
23
use Koha::StockRotationItem;
24
25
use base qw(Koha::Objects);
26
27
=head1 NAME
28
29
StockRotationItems - Koha StockRotationItems Object class
30
31
=head1 SYNOPSIS
32
33
StockRotationItems class used primarily by stockrotation .pls and the stock
34
rotation cron script.
35
36
=head1 DESCRIPTION
37
38
Standard Koha::Objects definitions, and additional methods.
39
40
=head1 API
41
42
=head2 Class Methods
43
44
=cut
45
46
=head3 _type
47
48
=cut
49
50
sub _type {
51
    return 'Stockrotationitem';
52
}
53
54
=head3 object_class
55
56
=cut
57
58
sub object_class {
59
    return 'Koha::StockRotationItem';
60
}
61
62
=head3 investigate
63
64
  my $report = $items->investigate;
65
66
Return a stockrotation report about this set of stockrotationitems.
67
68
In this part of the overall investigation process we split individual item
69
reports into appropriate action segments of our items report and increment
70
some counters.
71
72
The report generated here will be used on the stage level to slot our item
73
reports into appropriate sections of the branched report.
74
75
For details of intent and context of this procedure, please see
76
Koha::StockRotationRota->investigate.
77
78
=cut
79
80
sub investigate {
81
    my ( $self ) = @_;
82
83
    my $items_report = {
84
        items => [],
85
        log => [],
86
        initiable_items => [],
87
        repatriable_items => [],
88
        advanceable_items => [],
89
        indemand_items => [],
90
        actionable => 0,
91
        stationary => 0,
92
    };
93
    while ( my $item = $self->next ) {
94
        my $report = $item->investigate;
95
        if ( $report->{reason} eq 'initiation' ) {
96
            $items_report->{initiable}++;
97
            $items_report->{actionable}++;
98
            push @{$items_report->{items}}, $report;
99
            push @{$items_report->{initiable_items}}, $report;
100
        } elsif ( $report->{reason} eq 'repatriation' ) {
101
            $items_report->{repatriable}++;
102
            $items_report->{actionable}++;
103
            push @{$items_report->{items}}, $report;
104
            push @{$items_report->{repatriable_items}}, $report;
105
        } elsif ( $report->{reason} eq 'advancement' ) {
106
            $items_report->{actionable}++;
107
            push @{$items_report->{items}}, $report;
108
            push @{$items_report->{advanceable_items}}, $report;
109
        } elsif ( $report->{reason} eq 'in-demand' ) {
110
            $items_report->{actionable}++;
111
            push @{$items_report->{items}}, $report;
112
            push @{$items_report->{indemand_items}}, $report;
113
        } else {
114
            $items_report->{stationary}++;
115
            push @{$items_report->{log}}, $report;
116
        }
117
    }
118
119
    return $items_report;
120
}
121
122
1;
123
124
=head1 AUTHOR
125
126
Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
127
128
=cut
(-)a/Koha/StockRotationRota.pm (+182 lines)
Line 0 Link Here
1
package Koha::StockRotationRota;
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 under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
use Modern::Perl;
21
22
use Koha::Database;
23
use Koha::StockRotationStages;
24
use Koha::StockRotationItem;
25
use Koha::StockRotationItems;
26
27
use base qw(Koha::Object);
28
29
=head1 NAME
30
31
StockRotationRota - Koha StockRotationRota Object class
32
33
=head1 SYNOPSIS
34
35
StockRotationRota class used primarily by stockrotation .pls and the stock
36
rotation cron script.
37
38
=head1 DESCRIPTION
39
40
Standard Koha::Objects definitions, and additional methods.
41
42
=head1 API
43
44
=head2 Class Methods
45
46
=cut
47
48
=head3 stockrotationstages
49
50
  my $stages = Koha::StockRotationRota->stockrotationstages;
51
52
Returns the stages associated with the current rota.
53
54
=cut
55
56
sub stockrotationstages {
57
    my ( $self ) = @_;
58
    my $rs = $self->_result->stockrotationstages;
59
    return Koha::StockRotationStages->_new_from_dbic( $rs );
60
}
61
62
=head3 add_item
63
64
  my $rota = $rota->add_item($itemnumber);
65
66
Add item identified by $ITEMNUMBER to this rota, which means we associate it
67
with the first stage of this rota.  Should the item already be associated with
68
a rota, move it from that rota to this rota.
69
70
=cut
71
72
sub add_item {
73
    my ( $self, $itemnumber ) = @_;
74
    my $sritem = Koha::StockRotationItems->find($itemnumber);
75
    if ($sritem) {
76
        $sritem->stage_id($self->first_stage->stage_id)
77
            ->indemand(0)->fresh(1)->store;
78
    } else {
79
        $sritem = Koha::StockRotationItem->new({
80
            itemnumber_id => $itemnumber,
81
            stage_id      => $self->first_stage->stage_id,
82
            indemand      => 0,
83
            fresh         => 1,
84
        })->store;
85
    }
86
    return $self;
87
}
88
89
=head3 first_stage
90
91
  my $stage = $rota->first_stage;
92
93
Return the first stage attached to this rota (the one that has an undefined
94
`stagebefore`).
95
96
=cut
97
98
sub first_stage {
99
    my ( $self ) = @_;
100
    my $guess = $self->stockrotationstages->next;
101
    my $stage = $guess->first_sibling;
102
    return ( $stage ) ? $stage : $guess;
103
}
104
105
=head3 stockrotationitems
106
107
  my $items = $rota->stockrotationitems;
108
109
Return all items associated with this rota via its stages.
110
111
=cut
112
113
sub stockrotationitems {
114
    my ( $self ) = @_;
115
    my $rs = Koha::StockRotationItems->search(
116
        { 'stage.rota_id' => $self->rota_id }, { join =>  [ qw/stage/ ] }
117
    );
118
    return $rs;
119
}
120
121
=head3 investigate
122
123
  my $report = $rota->investigate($report_so_far);
124
125
Aim here is to return $report augmented with content for this rota.  We
126
delegate to $stage->investigate.
127
128
The report will include some basic information and 2 primary reports:
129
130
- per rota report in 'rotas'. This report is mainly used by admins to do check
131
  & compare results.
132
133
- branched report in 'branched'.  This is the workhorse: emails to libraries
134
  are compiled from these reports, and they will have the actionable work.
135
136
Both reports are generated in stage based investigations; the rota report is
137
then glued into place at this stage.
138
139
=cut
140
141
sub investigate {
142
    my ( $self, $report ) = @_;
143
    my $count = $self->stockrotationitems->count;
144
    $report->{sum_items} += $count;
145
146
    if ( $self->active ) {
147
        $report->{rotas_active}++;
148
        # stockrotationstages->investigate augments $report with the stage's
149
        # content.  This is how 'branched' slowly accumulates all items.
150
        $report = $self->stockrotationstages->investigate($report);
151
        # Add our rota report to the full report.
152
        push @{$report->{rotas}}, {
153
            name  => $self->title,
154
            id    => $self->rota_id,
155
            items => $report->{tmp_items} || [],
156
            log   => $report->{tmp_log} || [],
157
        };
158
        delete $report->{tmp_items};
159
        delete $report->{tmp_log};
160
    } else {                    # Rota is not active.
161
        $report->{rotas_inactive}++;
162
        $report->{items_inactive} += $count;
163
    }
164
165
    return $report;
166
}
167
168
=head3 _type
169
170
=cut
171
172
sub _type {
173
    return 'Stockrotationrota';
174
}
175
176
1;
177
178
=head1 AUTHOR
179
180
Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
181
182
=cut
(-)a/Koha/StockRotationRotas.pm (+105 lines)
Line 0 Link Here
1
package Koha::StockRotationRotas;
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 under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
use Modern::Perl;
21
22
use Koha::Database;
23
use Koha::StockRotationRota;
24
25
use base qw(Koha::Objects);
26
27
=head1 NAME
28
29
StockRotationRotas - Koha StockRotationRotas Object class
30
31
=head1 SYNOPSIS
32
33
StockRotationRotas class used primarily by stockrotation .pls and the stock
34
rotation cron script.
35
36
=head1 DESCRIPTION
37
38
Standard Koha::Objects definitions, and additional methods.
39
40
=head1 API
41
42
=head2 Class Methods
43
44
=cut
45
46
=head3 investigate
47
48
  my $report = $rotas->investigate;
49
50
Return a report detailing the current status and required actions for all
51
relevant items spread over rotas.
52
53
See Koha::StockRotationRota->investigate for details.
54
55
=cut
56
57
sub investigate {
58
    my ( $self ) = @_;
59
60
    my $report = {
61
        actionable     => 0,
62
        advanceable    => 0,
63
        initiable      => 0,
64
        items_inactive => 0,
65
        repatriable    => 0,
66
        rotas_active   => 0,
67
        rotas_inactive => 0,
68
        stationary     => 0,
69
        sum_items      => 0,
70
        sum_rotas      => $self->count,
71
        branched       => {},
72
        rotas          => [],
73
        items          => [],
74
    };
75
76
    while ( my $rota = $self->next ) {
77
        $report = $rota->investigate($report)
78
    }
79
80
    return $report;
81
}
82
83
=head3 _type
84
85
=cut
86
87
sub _type {
88
    return 'Stockrotationrota';
89
}
90
91
=head3 object_class
92
93
=cut
94
95
sub object_class {
96
    return 'Koha::StockRotationRota';
97
}
98
99
1;
100
101
=head1 AUTHOR
102
103
Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
104
105
=cut
(-)a/Koha/StockRotationStage.pm (+419 lines)
Line 0 Link Here
1
package Koha::StockRotationStage;
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 under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
use Modern::Perl;
21
22
use Koha::Database;
23
use Koha::Library;
24
use Koha::StockRotationRota;
25
26
use base qw(Koha::Object);
27
28
=head1 NAME
29
30
StockRotationStage - Koha StockRotationStage Object class
31
32
=head1 SYNOPSIS
33
34
StockRotationStage class used primarily by stockrotation .pls and the stock
35
rotation cron script.
36
37
=head1 DESCRIPTION
38
39
Standard Koha::Objects definitions, and additional methods.
40
41
=head1 API
42
43
=head2 Class Methods
44
45
=cut
46
47
=head3 _type
48
49
=cut
50
51
sub _type {
52
    return 'Stockrotationstage';
53
}
54
55
sub _relation {
56
    my ( $self, $method, $type ) = @_;
57
    return sub {
58
        my $rs = $self->_result->$method;
59
        return 0 if !$rs;
60
        my $namespace = 'Koha::' . $type;
61
        return $namespace->_new_from_dbic( $rs );
62
    }
63
}
64
65
=head3 stockrotationitems
66
67
  my $stages = Koha::StockRotationStage->stockrotationitems;
68
69
Returns the items associated with the current stage.
70
71
=cut
72
73
sub stockrotationitems {
74
    my ( $self ) = @_;
75
    return &{$self->_relation(qw/ stockrotationitems StockRotationItems /)};
76
}
77
78
=head3 branchcode
79
80
  my $branch = Koha::StockRotationStage->branchcode;
81
82
Returns the branch associated with the current stage.
83
84
=cut
85
86
sub branchcode {
87
    my ( $self ) = @_;
88
    return &{$self->_relation(qw/ branchcode Library /)};
89
}
90
91
=head3 rota
92
93
  my $rota = Koha::StockRotationStage->rota;
94
95
Returns the rota associated with the current stage.
96
97
=cut
98
99
sub rota {
100
    my ( $self ) = @_;
101
    return &{$self->_relation(qw/ rota StockRotationRota /)};
102
}
103
104
=head3 siblings
105
106
  my $siblings = $stage->siblings;
107
108
Koha::Object wrapper around DBIx::Class::Ordered.
109
110
=cut
111
112
sub siblings {
113
    my ( $self ) = @_;
114
    return &{$self->_relation(qw/ siblings StockRotationStages /)};
115
}
116
117
=head3 next_siblings
118
119
  my $next_siblings = $stage->next_siblings;
120
121
Koha::Object wrapper around DBIx::Class::Ordered.
122
123
=cut
124
125
sub next_siblings {
126
    my ( $self ) = @_;
127
    return &{$self->_relation(qw/ next_siblings StockRotationStages /)};
128
}
129
130
=head3 previous_siblings
131
132
  my $previous_siblings = $stage->previous_siblings;
133
134
Koha::Object wrapper around DBIx::Class::Ordered.
135
136
=cut
137
138
sub previous_siblings {
139
    my ( $self ) = @_;
140
    return &{$self->_relation(qw/ previous_siblings StockRotationStages /)};
141
}
142
143
=head3 next_sibling
144
145
  my $next = $stage->next_sibling;
146
147
Koha::Object wrapper around DBIx::Class::Ordered.
148
149
=cut
150
151
sub next_sibling {
152
    my ( $self ) = @_;
153
    return &{$self->_relation(qw/ next_sibling StockRotationStage /)};
154
}
155
156
=head3 previous_sibling
157
158
  my $previous = $stage->previous_sibling;
159
160
Koha::Object Wrapper around DBIx::Class::Ordered.
161
162
=cut
163
164
sub previous_sibling {
165
    my ( $self ) = @_;
166
    return &{$self->_relation(qw/ previous_sibling StockRotationStage /)};
167
}
168
169
=head3 first_sibling
170
171
  my $first = $stage->first_sibling;
172
173
Koha::Object Wrapper around DBIx::Class::Ordered.
174
175
=cut
176
177
sub first_sibling {
178
    my ( $self ) = @_;
179
    return &{$self->_relation(qw/ first_sibling StockRotationStage /)};
180
}
181
182
=head3 last_sibling
183
184
  my $last = $stage->last_sibling;
185
186
Koha::Object Wrapper around DBIx::Class::Ordered.
187
188
=cut
189
190
sub last_sibling {
191
    my ( $self ) = @_;
192
    return &{$self->_relation(qw/ last_sibling StockRotationStage /)};
193
}
194
195
=head3 move_previous
196
197
  1|0 = $stage->move_previous;
198
199
Koha::Object Wrapper around DBIx::Class::Ordered.
200
201
=cut
202
203
sub move_previous {
204
    my ( $self ) = @_;
205
    return $self->_result->move_previous;
206
}
207
208
=head3 move_next
209
210
  1|0 = $stage->move_next;
211
212
Koha::Object Wrapper around DBIx::Class::Ordered.
213
214
=cut
215
216
sub move_next {
217
    my ( $self ) = @_;
218
    return $self->_result->move_next;
219
}
220
221
=head3 move_first
222
223
  1|0 = $stage->move_first;
224
225
Koha::Object Wrapper around DBIx::Class::Ordered.
226
227
=cut
228
229
sub move_first {
230
    my ( $self ) = @_;
231
    return $self->_result->move_first;
232
}
233
234
=head3 move_last
235
236
  1|0 = $stage->move_last;
237
238
Koha::Object Wrapper around DBIx::Class::Ordered.
239
240
=cut
241
242
sub move_last {
243
    my ( $self ) = @_;
244
    return $self->_result->move_last;
245
}
246
247
=head3 move_to
248
249
  1|0 = $stage->move_to($position);
250
251
Koha::Object Wrapper around DBIx::Class::Ordered.
252
253
=cut
254
255
sub move_to {
256
    my ( $self, $position ) = @_;
257
    return $self->_result->move_to($position)
258
        if ( $position le $self->rota->stockrotationstages->count );
259
    return 0;
260
}
261
262
=head3 move_to_group
263
264
  1|0 = $stage->move_to_group($rota_id, [$position]);
265
266
Koha::Object Wrapper around DBIx::Class::Ordered.
267
268
=cut
269
270
sub move_to_group {
271
    my ( $self, $rota_id, $position ) = @_;
272
    return $self->_result->move_to_group($rota_id, $position);
273
}
274
275
=head3 delete
276
277
  1|0 = $stage->delete;
278
279
Koha::Object Wrapper around DBIx::Class::Ordered.
280
281
=cut
282
283
sub delete {
284
    my ( $self ) = @_;
285
    return $self->_result->delete;
286
}
287
288
=head3 investigate
289
290
  my $report = $stage->investigate($report_so_far);
291
292
Return a stage based report.  This report will mutate and augment the report
293
that is passed to it.  It slots item reports into the branched and temporary
294
rota sections of the report.  It also increments a number of counters.
295
296
For details of intent and context of this procedure, please see
297
Koha::StockRotationRota->investigate.
298
299
=cut
300
301
sub investigate {
302
    my ( $self, $report ) = @_;
303
    my $new_stage = $self->next_sibling;
304
    my $duration = $self->duration;
305
    # Generate stage items report
306
    my $items_report = $self->stockrotationitems->investigate;
307
308
    # Merge into general report
309
310
    ## Branched indexes
311
    ### The branched indexes work as follows:
312
    ### - They contain information about the relevant branch
313
    ### - They contain an index of actionable items for that branch
314
    ### - They contain an index of non-actionable items for that branch
315
316
    ### Items are assigned to a particular branched index as follows:
317
    ### - 'advanceable' : assigned to branch of the current stage
318
    ###   (this should also be the current holding branch)
319
    ### - 'log' items are always assigned to branch of current stage.
320
    ### - 'indemand' : assigned to branch of current stage
321
    ###   (this should also be the current holding branch)
322
    ### - 'initiable' : assigned to the current holding branch of item
323
    ### - 'repatriable' : assigned to the current holding branch of item
324
325
    ### 'Advanceable', 'log', 'indemand':
326
327
    # Set up our stage branch info.
328
    my $stagebranch = $self->_result->branchcode;
329
    my $stagebranchcode = $stagebranch->branchcode;
330
331
    # Initiate our stage branch index if it does not yet exist.
332
    if ( !$report->{branched}->{$stagebranchcode} ) {
333
        $report->{branched}->{$stagebranchcode} = {
334
            code  => $stagebranchcode,
335
            name  => $stagebranch->branchname,
336
            email => $stagebranch->branchreplyto
337
              ? $stagebranch->branchreplyto
338
              : $stagebranch->branchemail,
339
            phone => $stagebranch->branchphone,
340
            items => [],
341
            log => [],
342
        };
343
    }
344
345
    push @{$report->{branched}->{$stagebranchcode}->{items}},
346
        @{$items_report->{advanceable_items}};
347
    push @{$report->{branched}->{$stagebranchcode}->{log}},
348
        @{$items_report->{log}};
349
    push @{$report->{branched}->{$stagebranchcode}->{items}},
350
        @{$items_report->{indemand_items}};
351
352
    ### 'Initiable' & 'Repatriable'
353
    foreach my $ireport (@{$items_report->{initiable_items}}) {
354
        my $branch = $ireport->{branch};
355
        my $branchcode = $branch->branchcode;
356
        if ( !$report->{branched}->{$branchcode} ) {
357
            $report->{branched}->{$branchcode} = {
358
                code  => $branchcode,
359
                name  => $branch->branchname,
360
                email => $stagebranch->branchreplyto
361
                  ? $stagebranch->branchreplyto
362
                  : $stagebranch->branchemail,
363
                phone => $branch->branchphone,
364
                items => [],
365
                log => [],
366
            };
367
        }
368
        push @{$report->{branched}->{$branchcode}->{items}}, $ireport;
369
    }
370
371
    foreach my $ireport (@{$items_report->{repatriable_items}}) {
372
        my $branch = $ireport->{branch};
373
        my $branchcode = $branch->branchcode;
374
        if ( !$report->{branched}->{$branchcode} ) {
375
            $report->{branched}->{$branchcode} = {
376
                code  => $branchcode,
377
                name  => $branch->branchname,
378
                email => $stagebranch->branchreplyto
379
                  ? $stagebranch->branchreplyto
380
                  : $stagebranch->branchemail,
381
                phone => $branch->branchphone,
382
                items => [],
383
                log => [],
384
            };
385
        }
386
        push @{$report->{branched}->{$branchcode}->{items}}, $ireport;
387
    }
388
389
    ## Per rota indexes
390
    ### Per rota indexes are item reports pushed into the index for the
391
    ### current rota.  We don't know where that index is yet as we don't know
392
    ### about the current rota.  To resolve this we assign our items and log
393
    ### to tmp indexes.  They will be merged into the proper rota index at the
394
    ### rota level.
395
    push @{$report->{tmp_items}}, @{$items_report->{items}};
396
    push @{$report->{tmp_log}}, @{$items_report->{log}};
397
398
    ## Collection of items
399
    ### Finally we just add our collection of items to the full item index.
400
    push @{$report->{items}}, @{$items_report->{items}};
401
402
    ## Assemble counters
403
    $report->{actionable} += $items_report->{actionable};
404
    $report->{indemand} += scalar @{$items_report->{indemand_items}};
405
    $report->{advanceable} += scalar @{$items_report->{advanceable_items}};
406
    $report->{initiable} += scalar @{$items_report->{initiable_items}};
407
    $report->{repatriable} += scalar @{$items_report->{repatriable_items}};
408
    $report->{stationary} += scalar @{$items_report->{log}};
409
410
    return $report;
411
}
412
413
1;
414
415
=head1 AUTHOR
416
417
Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
418
419
=cut
(-)a/Koha/StockRotationStages.pm (+90 lines)
Line 0 Link Here
1
package Koha::StockRotationStages;
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 under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
use Modern::Perl;
21
22
use Koha::Database;
23
use Koha::StockRotationStage;
24
25
use base qw(Koha::Objects);
26
27
=head1 NAME
28
29
StockRotationStages - Koha StockRotationStages Object class
30
31
=head1 SYNOPSIS
32
33
StockRotationStages class used primarily by stockrotation .pls and the stock
34
rotation cron script.
35
36
=head1 DESCRIPTION
37
38
Standard Koha::Objects definitions, and additional methods.
39
40
=head1 API
41
42
=head2 Class Methods
43
44
=cut
45
46
=head3 investigate
47
48
  my $report = $stages->investigate($rota_so_far);
49
50
Return a report detailing the current status and required actions for all
51
relevant items spread over the set of stages.
52
53
For details of intent and context of this procedure, please see
54
Koha::StockRotationRota->investigate.
55
56
=cut
57
58
sub investigate {
59
    my ( $self, $report ) = @_;
60
61
    while ( my $stage = $self->next ) {
62
        $report = $stage->investigate($report);
63
    }
64
65
    return $report;
66
}
67
68
=head3 _type
69
70
=cut
71
72
sub _type {
73
    return 'Stockrotationstage';
74
}
75
76
=head3 object_class
77
78
=cut
79
80
sub object_class {
81
    return 'Koha::StockRotationStage';
82
}
83
84
1;
85
86
=head1 AUTHOR
87
88
Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
89
90
=cut
(-)a/Koha/Util/StockRotation.pm (+247 lines)
Line 0 Link Here
1
package Koha::Util::StockRotation;
2
3
# Module contains subroutines used with Stock Rotation
4
#
5
# Copyright 2016 PTFS-Europe Ltd
6
#
7
# This file is part of Koha.
8
#
9
# Koha is free software; you can redistribute it and/or modify it under the
10
# terms of the GNU General Public License as published by the Free Software
11
# Foundation; either version 3 of the License, or (at your option) any later
12
# version.
13
#
14
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
15
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
17
#
18
# You should have received a copy of the GNU General Public License along
19
# with Koha; if not, write to the Free Software Foundation, Inc.,
20
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
22
use Modern::Perl;
23
24
use Koha::Items;
25
use Koha::StockRotationItems;
26
use Koha::Database;
27
28
our ( @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS );
29
BEGIN {
30
    require Exporter;
31
    @ISA = qw( Exporter );
32
    @EXPORT = qw( );
33
    @EXPORT_OK = qw(
34
        get_branches
35
        get_stages
36
        toggle_indemand
37
        remove_from_stage
38
        get_barcodes_status
39
        add_items_to_rota
40
        move_to_next_stage
41
    );
42
    %EXPORT_TAGS = ( ALL => [ @EXPORT_OK, @EXPORT ] );
43
}
44
45
=head1 NAME
46
47
Koha::Util::StockRotation - utility class with routines for Stock Rotation
48
49
=head1 FUNCTIONS
50
51
=head2 get_branches
52
53
    returns all branches ordered by branchname as an array, each element
54
    contains a hashref containing branch details
55
56
=cut
57
58
sub get_branches {
59
60
    return Koha::Libraries->search(
61
        {},
62
        { order_by => ['branchname'] }
63
    )->unblessed;
64
65
}
66
67
=head2 get_stages
68
69
    returns an arrayref of StockRotationStage objects representing
70
    all stages for a passed rota
71
72
=cut
73
74
sub get_stages {
75
76
    my $rota = shift;
77
78
    my @out = ();
79
80
    if ($rota->stockrotationstages->count > 0) {
81
82
        push @out, $rota->first_stage->unblessed;
83
84
        push @out, @{$rota->first_stage->siblings->unblessed};
85
86
    }
87
88
    return \@out;
89
}
90
91
=head2 toggle_indemand
92
93
    given an item's ID & stage ID toggle that item's in_demand
94
    status on that stage
95
96
=cut
97
98
sub toggle_indemand {
99
100
    my ($item_id, $stage_id) = @_;
101
102
    # Get the item object
103
    my $item = Koha::StockRotationItems->find(
104
        {
105
            itemnumber_id => $item_id,
106
            stage_id      => $stage_id
107
        }
108
    );
109
110
    # Toggle the item's indemand flag
111
    my $new_indemand = ($item->indemand == 1) ? 0 : 1;
112
113
    $item->indemand($new_indemand)->store;
114
115
}
116
117
=head2 move_to_next_stage
118
119
    given an item's ID and stage ID, move it
120
    to the next stage on the rota
121
122
=cut
123
124
sub move_to_next_stage {
125
126
    my ($item_id, $stage_id) = shift;
127
128
    # Get the item object
129
    my $item = Koha::StockRotationItems->find(
130
        {
131
            itemnumber_id => $item_id,
132
            stage_id      => $stage_id
133
        }
134
    );
135
136
    $item->advance;
137
138
}
139
140
=head2 remove_from_stage
141
142
    given an item's ID & stage ID, remove that item from that stage
143
144
=cut
145
146
sub remove_from_stage {
147
148
    my ($item_id, $stage_id) = @_;
149
150
    # Get the item object and delete it
151
    Koha::StockRotationItems->find(
152
        {
153
            itemnumber_id => $item_id,
154
            stage_id      => $stage_id
155
        }
156
    )->delete;
157
158
}
159
160
=head2 get_barcodes_status
161
162
    take an arrayref of barcodes and a status hashref and populate it
163
164
=cut
165
166
sub get_barcodes_status {
167
168
    my ($rota_id, $barcodes, $status) = @_;
169
170
    # Get the items associated with these barcodes
171
    my $items = Koha::Items->search(
172
        {
173
            barcode => { '-in' => $barcodes }
174
        },
175
        {
176
            prefetch => 'stockrotationitem'
177
        }
178
    );
179
    # Get an array of barcodes that were found
180
    # Assign each barcode's status
181
    my @found = ();
182
    while (my $item = $items->next) {
183
184
        push @found, $item->barcode if $item->barcode;
185
186
        # Check if it's on a rota
187
        my $on_rota = $item->stockrotationitem;
188
189
        # It is on a rota
190
        if ($on_rota) {
191
192
            # Check if it's on this rota
193
            if ($on_rota->stage->rota->rota_id == $rota_id) {
194
195
                # It's on this rota
196
                push @{$status->{on_this}}, $item;
197
198
            } else {
199
200
                # It's on another rota
201
                push @{$status->{on_other}}, $item;
202
203
            }
204
205
        } else {
206
207
            # Item is not on a rota
208
            push @{$status->{ok}}, $item;
209
210
        }
211
212
    }
213
214
    # Create an array of barcodes supplied in the file that
215
    # were not found in the catalogue
216
    my %found_in_cat = map{ $_ => 1 } @found;
217
    push @{$status->{not_found}}, grep(
218
        !defined $found_in_cat{$_}, @{$barcodes}
219
    );
220
221
}
222
223
=head2 add_items_to_rota
224
225
    take an arrayref of Koha::Item objects and add them to the passed rota
226
227
=cut
228
229
sub add_items_to_rota {
230
231
    my ($rota_id, $items) = @_;
232
233
    foreach my $item(@{$items}) {
234
235
        $item->add_to_rota($rota_id);
236
237
    }
238
239
}
240
241
1;
242
243
=head1 AUTHOR
244
245
Andrew Isherwood <andrew.isherwood@ptfs-europe.com>
246
247
=cut
(-)a/api/v1/swagger/paths.json (+3 lines)
Lines 31-35 Link Here
31
  },
31
  },
32
  "/illrequests": {
32
  "/illrequests": {
33
    "$ref": "paths/illrequests.json#/~1illrequests"
33
    "$ref": "paths/illrequests.json#/~1illrequests"
34
  },
35
  "/rotas/{rota_id}/stages/{stage_id}/position": {
36
    "$ref": "paths/rotas.json#/~1rotas~1{rota_id}~1stages~1{stage_id}~1position"
34
  }
37
  }
35
}
38
}
(-)a/api/v1/swagger/paths/rotas.json (+79 lines)
Line 0 Link Here
1
{
2
    "/rotas/{rota_id}/stages/{stage_id}/position": {
3
        "put": {
4
            "x-mojo-to": "Stage#move",
5
            "operationId": "moveStage",
6
            "tags": ["rotas"],
7
            "parameters": [{
8
                "name": "rota_id",
9
                "in": "path",
10
                "required": true,
11
                "description": "A rotas ID",
12
                "type": "integer"
13
            }, {
14
                "name": "stage_id",
15
                "in": "path",
16
                "required": true,
17
                "description": "A stages ID",
18
                "type": "integer"
19
            }, {
20
                "name": "position",
21
                "in": "body",
22
                "required": true,
23
                "description": "A stages position in the rota",
24
                "schema": {
25
                    "type": "integer"
26
                }
27
            }],
28
            "produces": [
29
                "application/json"
30
            ],
31
            "responses": {
32
                "200": {
33
                    "description": "OK"
34
                },
35
                "400": {
36
                    "description": "Bad request",
37
                    "schema": {
38
                        "$ref": "../definitions.json#/error"
39
                    }
40
                },
41
                "401": {
42
                    "description": "Authentication required",
43
                    "schema": {
44
                        "$ref": "../definitions.json#/error"
45
                    }
46
                },
47
                "403": {
48
                    "description": "Access forbidden",
49
                    "schema": {
50
                        "$ref": "../definitions.json#/error"
51
                    }
52
                },
53
                "404": {
54
                    "description": "Position not found",
55
                    "schema": {
56
                        "$ref": "../definitions.json#/error"
57
                    }
58
                },
59
                "500": {
60
                    "description": "Internal server error",
61
                    "schema": {
62
                        "$ref": "../definitions.json#/error"
63
                    }
64
                },
65
                "503": {
66
                    "description": "Under maintenance",
67
                    "schema": {
68
                        "$ref": "../definitions.json#/error"
69
                    }
70
                }
71
            },
72
            "x-koha-authorization": {
73
                "permissions": {
74
                    "borrowers": "1"
75
                }
76
            }
77
        }
78
    }
79
}
(-)a/catalogue/stockrotation.pl (+179 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# Copyright 2016 PTFS-Europe Ltd
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
=head1 stockrotation.pl
21
22
 Script to manage item assignments to stock rotation rotas. Including their
23
 assiciated stages
24
25
=cut
26
27
use Modern::Perl;
28
use CGI;
29
30
use C4::Auth;
31
use C4::Output;
32
use C4::Search;
33
34
use Koha::Biblio;
35
use Koha::Item;
36
use Koha::StockRotationRotas;
37
use Koha::StockRotationStages;
38
use Koha::Util::StockRotation qw(:ALL);
39
40
my $input = new CGI;
41
42
unless (C4::Context->preference('StockRotation')) {
43
    # redirect to Intranet home if self-check is not enabled
44
    print $input->redirect("/cgi-bin/koha/mainpage.pl");
45
    exit;
46
}
47
48
my %params = $input->Vars();
49
50
my $op = $params{op};
51
52
my $biblionumber = $input->param('biblionumber');
53
54
my ($template, $loggedinuser, $cookie) = get_template_and_user(
55
    {
56
        template_name   => 'catalogue/stockrotation.tt',
57
        query           => $input,
58
        type            => 'intranet',
59
        authnotrequired => 0,
60
        flagsrequired   => {
61
            catalogue => 1,
62
            stockrotation => 'manage_rota_items',
63
        },
64
    }
65
);
66
67
if (!defined $op) {
68
69
    # List all items along with their associated rotas
70
    my $biblio = Koha::Biblios->find($biblionumber);
71
72
    my $items = $biblio->items;
73
74
    # Get only rotas with stages
75
    my $rotas = Koha::StockRotationRotas->search(
76
        {
77
            'stockrotationstages.stage_id' => { '!=', undef }
78
        },
79
        {
80
            join     => 'stockrotationstages',
81
            collapse => 1,
82
            order_by => 'title'
83
        }
84
    );
85
86
    # Construct a model to pass to the view
87
    my @item_data = ();
88
89
    while (my $item = $items->next) {
90
91
        my $item_hashref = {
92
            bib_item   => $item
93
        };
94
95
        my $stockrotationitem = $item->stockrotationitem;
96
97
        # If this item is on a rota
98
        if ($stockrotationitem != 0) {
99
100
            # This item's rota
101
            my $rota = $stockrotationitem->stage->rota;
102
103
            # This rota's stages
104
            my $stages = get_stages($rota);
105
106
            $item_hashref->{rota} = $rota;
107
108
            $item_hashref->{stockrotationitem} = $stockrotationitem;
109
110
            $item_hashref->{stages} = $stages;
111
112
        }
113
114
        push @item_data, $item_hashref;
115
116
    }
117
118
    $template->param(
119
        no_op_set         => 1,
120
        rotas             => $rotas,
121
        items             => \@item_data,
122
        branches          => get_branches(),
123
        biblio            => $biblio,
124
        biblionumber      => $biblio->biblionumber,
125
        stockrotationview => 1,
126
        C4::Search::enabled_staff_search_views
127
    );
128
129
} elsif ($op eq "toggle_in_demand") {
130
131
    # Toggle in demand
132
    toggle_indemand($params{item_id}, $params{stage_id});
133
134
    # Return to items list
135
    print $input->redirect("?biblionumber=$biblionumber");
136
137
} elsif ($op eq "remove_item_from_stage") {
138
139
    # Remove from the stage
140
    remove_from_stage($params{item_id}, $params{stage_id});
141
142
    # Return to items list
143
    print $input->redirect("?biblionumber=$biblionumber");
144
145
} elsif ($op eq "move_to_next_stage") {
146
147
    move_to_next_stage($params{item_id}, $params{stage_id});
148
149
    # Return to items list
150
    print $input->redirect("?biblionumber=" . $params{biblionumber});
151
152
} elsif ($op eq "add_item_to_rota") {
153
154
    my $item = Koha::Items->find($params{item_id});
155
156
    $item->add_to_rota($params{rota_id});
157
158
    print $input->redirect("?biblionumber=" . $params{biblionumber});
159
160
} elsif ($op eq "confirm_remove_from_rota") {
161
162
    $template->param(
163
        op                => $params{op},
164
        stage_id          => $params{stage_id},
165
        item_id           => $params{item_id},
166
        biblionumber      => $params{biblionumber},
167
        stockrotationview => 1,
168
        C4::Search::enabled_staff_search_views
169
    );
170
171
}
172
173
output_html_with_http_headers $input, $cookie, $template->output;
174
175
=head1 AUTHOR
176
177
Andrew Isherwood <andrew.isherwood@ptfs-europe.com>
178
179
=cut
(-)a/installer/data/mysql/atomicupdate/stockrot_tables.sql (-2 / +2 lines)
Lines 28-34 CREATE TABLE IF NOT EXISTS stockrotationstages ( Link Here
28
      FOREIGN KEY (`branchcode_id`)
28
      FOREIGN KEY (`branchcode_id`)
29
      REFERENCES `branches` (`branchcode`)
29
      REFERENCES `branches` (`branchcode`)
30
      ON UPDATE CASCADE ON DELETE CASCADE
30
      ON UPDATE CASCADE ON DELETE CASCADE
31
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
31
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
32
32
33
-- Stock Rotation Items
33
-- Stock Rotation Items
34
34
Lines 46-52 CREATE TABLE IF NOT EXISTS stockrotationitems ( Link Here
46
      FOREIGN KEY (`stage_id`)
46
      FOREIGN KEY (`stage_id`)
47
      REFERENCES `stockrotationstages` (`stage_id`)
47
      REFERENCES `stockrotationstages` (`stage_id`)
48
      ON UPDATE CASCADE ON DELETE CASCADE
48
      ON UPDATE CASCADE ON DELETE CASCADE
49
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
49
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
50
50
51
-- System preferences
51
-- System preferences
52
52
(-)a/installer/data/mysql/en/mandatory/sample_notices.sql (-1 / +2 lines)
Lines 176-183 INSERT INTO `letter` (`module`, `code`, `branchcode`, `name`, `is_html`, `title` Link Here
176
('circulation', 'AR_SLIP', '', 'Article request - print slip', 0, 'Article request', 'Article request:\r\n\r\n<<borrowers.firstname>> <<borrowers.surname>> (<<borrowers.cardnumber>>),\r\n\r\nTitle: <<biblio.title>>\r\nBarcode: <<items.barcode>>\r\n\r\nArticle requested:\r\nTitle: <<article_requests.title>>\r\nAuthor: <<article_requests.author>>\r\nVolume: <<article_requests.volume>>\r\nIssue: <<article_requests.issue>>\r\nDate: <<article_requests.date>>\r\nPages: <<article_requests.pages>>\r\nChapters: <<article_requests.chapters>>\r\nNotes: <<article_requests.patron_notes>>\r\n', 'print'),
176
('circulation', 'AR_SLIP', '', 'Article request - print slip', 0, 'Article request', 'Article request:\r\n\r\n<<borrowers.firstname>> <<borrowers.surname>> (<<borrowers.cardnumber>>),\r\n\r\nTitle: <<biblio.title>>\r\nBarcode: <<items.barcode>>\r\n\r\nArticle requested:\r\nTitle: <<article_requests.title>>\r\nAuthor: <<article_requests.author>>\r\nVolume: <<article_requests.volume>>\r\nIssue: <<article_requests.issue>>\r\nDate: <<article_requests.date>>\r\nPages: <<article_requests.pages>>\r\nChapters: <<article_requests.chapters>>\r\nNotes: <<article_requests.patron_notes>>\r\n', 'print'),
177
('circulation', 'AR_PROCESSING', '', 'Article request - processing', 0, 'Article request processing', 'Dear <<borrowers.firstname>> <<borrowers.surname>> (<<borrowers.cardnumber>>),\r\n\r\nWe are now processing your request for an article from <<biblio.title>> (<<items.barcode>>).\r\n\r\nArticle requested:\r\nTitle: <<article_requests.title>>\r\nAuthor: <<article_requests.author>>\r\nVolume: <<article_requests.volume>>\r\nIssue: <<article_requests.issue>>\r\nDate: <<article_requests.date>>\r\nPages: <<article_requests.pages>>\r\nChapters: <<article_requests.chapters>>\r\nNotes: <<article_requests.patron_notes>>\r\n\r\nThank you!', 'email'),
177
('circulation', 'AR_PROCESSING', '', 'Article request - processing', 0, 'Article request processing', 'Dear <<borrowers.firstname>> <<borrowers.surname>> (<<borrowers.cardnumber>>),\r\n\r\nWe are now processing your request for an article from <<biblio.title>> (<<items.barcode>>).\r\n\r\nArticle requested:\r\nTitle: <<article_requests.title>>\r\nAuthor: <<article_requests.author>>\r\nVolume: <<article_requests.volume>>\r\nIssue: <<article_requests.issue>>\r\nDate: <<article_requests.date>>\r\nPages: <<article_requests.pages>>\r\nChapters: <<article_requests.chapters>>\r\nNotes: <<article_requests.patron_notes>>\r\n\r\nThank you!', 'email'),
178
('circulation', 'CHECKOUT_NOTE', '', 'Checkout note on item set by patron', '0', 'Checkout note', '<<borrowers.firstname>> <<borrowers.surname>> has added a note to the item <<biblio.title>> - <<biblio.author>> (<<biblio.biblionumber>>).','email');
178
('circulation', 'CHECKOUT_NOTE', '', 'Checkout note on item set by patron', '0', 'Checkout note', '<<borrowers.firstname>> <<borrowers.surname>> has added a note to the item <<biblio.title>> - <<biblio.author>> (<<biblio.biblionumber>>).','email');
179
180
INSERT INTO `letter` (`module`, `code`, `branchcode`, `name`, `is_html`, `title`, `content`, `message_transport_type`, `lang`)
179
INSERT INTO `letter` (`module`, `code`, `branchcode`, `name`, `is_html`, `title`, `content`, `message_transport_type`, `lang`)
181
    VALUES
180
    VALUES
182
        ('circulation', 'ACCOUNT_PAYMENT', '', 'Account payment', 0, 'Account payment', '[%- USE Price -%]\r\nA payment of [% credit.amount * -1 | $Price %] has been applied to your account.\r\n\r\nThis payment affected the following fees:\r\n[%- FOREACH o IN offsets %]\r\nDescription: [% o.debit.description %]\r\nAmount paid: [% o.amount * -1 | $Price %]\r\nAmount remaining: [% o.debit.amountoutstanding | $Price %]\r\n[% END %]', 'email', 'default'),
181
        ('circulation', 'ACCOUNT_PAYMENT', '', 'Account payment', 0, 'Account payment', '[%- USE Price -%]\r\nA payment of [% credit.amount * -1 | $Price %] has been applied to your account.\r\n\r\nThis payment affected the following fees:\r\n[%- FOREACH o IN offsets %]\r\nDescription: [% o.debit.description %]\r\nAmount paid: [% o.amount * -1 | $Price %]\r\nAmount remaining: [% o.debit.amountoutstanding | $Price %]\r\n[% END %]', 'email', 'default'),
183
            ('circulation', 'ACCOUNT_WRITEOFF', '', 'Account writeoff', 0, 'Account writeoff', '[%- USE Price -%]\r\nAn account writeoff of [% credit.amount * -1 | $Price %] has been applied to your account.\r\n\r\nThis writeoff affected the following fees:\r\n[%- FOREACH o IN offsets %]\r\nDescription: [% o.debit.description %]\r\nAmount paid: [% o.amount * -1 | $Price %]\r\nAmount remaining: [% o.debit.amountoutstanding | $Price %]\r\n[% END %]', 'email', 'default');
182
            ('circulation', 'ACCOUNT_WRITEOFF', '', 'Account writeoff', 0, 'Account writeoff', '[%- USE Price -%]\r\nAn account writeoff of [% credit.amount * -1 | $Price %] has been applied to your account.\r\n\r\nThis writeoff affected the following fees:\r\n[%- FOREACH o IN offsets %]\r\nDescription: [% o.debit.description %]\r\nAmount paid: [% o.amount * -1 | $Price %]\r\nAmount remaining: [% o.debit.amountoutstanding | $Price %]\r\n[% END %]', 'email', 'default');
183
INSERT INTO `letter` (`module`, `code`, `branchcode`, `name`, `is_html`, `title`, `content`, `message_transport_type`) VALUES
184
('circulation', 'SR_SLIP', '', 'Stock Rotation Slip', 0, 'Stockrotation Report', 'Stockrotation report for [% branch.name %]:\r\n\r\n[% IF branch.items.size %][% branch.items.size %] items to be processed for this branch.\r\n[% ELSE %]No items to be processed for this branch\r\n[% END %][% FOREACH item IN branch.items %][% IF item.reason ne \'in-demand\' %]Title: [% item.title %]\r\nAuthor: [% item.author %]\r\nCallnumber: [% item.callnumber %]\r\nLocation: [% item.location %]\r\nBarcode: [% item.barcode %]\r\nOn loan?: [% item.onloan %]\r\nStatus: [% item.reason %]\r\nCurrent Library: [% item.branch.branchname %] [% item.branch.branchcode %]\r\n\r\n[% END %][% END %]', 'email');
(-)a/installer/data/mysql/kohastructure.sql (-36 / +36 lines)
Lines 4167-4206 CREATE TABLE illrequests ( Link Here
4167
      ON UPDATE CASCADE ON DELETE CASCADE
4167
      ON UPDATE CASCADE ON DELETE CASCADE
4168
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
4168
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
4169
4169
4170
--
4171
-- Table structure for table `stockrotationrotas`
4172
--
4173
4174
CREATE TABLE IF NOT EXISTS stockrotationrotas (
4175
    rota_id int(11) auto_increment,          -- Stockrotation rota ID
4176
    title varchar(100) NOT NULL,            -- Title for this rota
4177
    description text NOT NULL default '',   -- Description for this rota
4178
    cyclical tinyint(1) NOT NULL default 0, -- Should items on this rota keep cycling?
4179
    active tinyint(1) NOT NULL default 0,   -- Is this rota currently active?
4180
    PRIMARY KEY (`rota_id`)
4181
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
4182
4183
--
4184
-- Table structure for table `stockrotationstages`
4185
--
4186
4187
CREATE TABLE IF NOT EXISTS stockrotationstages (
4188
    stage_id int(11) auto_increment,     -- Unique stage ID
4189
    position int(11) NOT NULL,           -- The position of this stage within its rota
4190
    rota_id int(11) NOT NULL,            -- The rota this stage belongs to
4191
    branchcode_id varchar(10) NOT NULL,  -- Branch this stage relates to
4192
    duration int(11) NOT NULL default 4, -- The number of days items shoud occupy this stage
4193
    PRIMARY KEY (`stage_id`),
4194
    CONSTRAINT `stockrotationstages_rifk`
4195
      FOREIGN KEY (`rota_id`)
4196
      REFERENCES `stockrotationrotas` (`rota_id`)
4197
      ON UPDATE CASCADE ON DELETE CASCADE,
4198
    CONSTRAINT `stockrotationstages_bifk`
4199
      FOREIGN KEY (`branchcode_id`)
4200
      REFERENCES `branches` (`branchcode`)
4201
      ON UPDATE CASCADE ON DELETE CASCADE
4202
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
4203
4204
--
4170
--
4205
-- Table structure for table `illrequestattributes`
4171
-- Table structure for table `illrequestattributes`
4206
--
4172
--
Lines 4230-4236 CREATE TABLE library_groups ( Link Here
4230
    description MEDIUMTEXT NULL DEFAULT NULL,    -- Longer explanation of the group, if necessary
4196
    description MEDIUMTEXT NULL DEFAULT NULL,    -- Longer explanation of the group, if necessary
4231
    ft_hide_patron_info tinyint(1) NOT NULL DEFAULT 0, -- Turn on the feature "Hide patron's info" for this group
4197
    ft_hide_patron_info tinyint(1) NOT NULL DEFAULT 0, -- Turn on the feature "Hide patron's info" for this group
4232
    ft_search_groups_opac tinyint(1) NOT NULL DEFAULT 0, -- Use this group for staff side search groups
4198
    ft_search_groups_opac tinyint(1) NOT NULL DEFAULT 0, -- Use this group for staff side search groups
4233
    ft_search_groups_staff tinyint(1) NOT NULL DEFAULT 0, -- Use this group for opac side search groups
4199
     ft_search_groups_staff tinyint(1) NOT NULL DEFAULT 0, -- Use this group for opac side search groups
4234
    created_on TIMESTAMP NULL,             -- Date and time of creation
4200
    created_on TIMESTAMP NULL,             -- Date and time of creation
4235
    updated_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- Date and time of last
4201
    updated_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- Date and time of last
4236
    PRIMARY KEY id ( id ),
4202
    PRIMARY KEY id ( id ),
Lines 4251-4256 CREATE TABLE `oauth_access_tokens` ( Link Here
4251
    PRIMARY KEY (`access_token`)
4217
    PRIMARY KEY (`access_token`)
4252
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
4218
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
4253
4219
4220
--
4221
-- Table structure for table `stockrotationrotas`
4222
--
4223
4224
CREATE TABLE IF NOT EXISTS stockrotationrotas (
4225
    rota_id int(11) auto_increment,          -- Stockrotation rota ID
4226
    title varchar(100) NOT NULL,            -- Title for this rota
4227
    description text NOT NULL default '',   -- Description for this rota
4228
    cyclical tinyint(1) NOT NULL default 0, -- Should items on this rota keep cycling?
4229
    active tinyint(1) NOT NULL default 0,   -- Is this rota currently active?
4230
    PRIMARY KEY (`rota_id`)
4231
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
4232
4233
--
4234
-- Table structure for table `stockrotationstages`
4235
--
4236
4237
CREATE TABLE IF NOT EXISTS stockrotationstages (
4238
    stage_id int(11) auto_increment,     -- Unique stage ID
4239
    position int(11) NOT NULL,           -- The position of this stage within its rota
4240
    rota_id int(11) NOT NULL,            -- The rota this stage belongs to
4241
    branchcode_id varchar(10) NOT NULL,  -- Branch this stage relates to
4242
    duration int(11) NOT NULL default 4, -- The number of days items shoud occupy this stage
4243
    PRIMARY KEY (`stage_id`),
4244
    CONSTRAINT `stockrotationstages_rifk`
4245
      FOREIGN KEY (`rota_id`)
4246
      REFERENCES `stockrotationrotas` (`rota_id`)
4247
      ON UPDATE CASCADE ON DELETE CASCADE,
4248
    CONSTRAINT `stockrotationstages_bifk`
4249
      FOREIGN KEY (`branchcode_id`)
4250
      REFERENCES `branches` (`branchcode`)
4251
      ON UPDATE CASCADE ON DELETE CASCADE
4252
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
4253
4254
--
4254
--
4255
-- Table structure for table `stockrotationitems`
4255
-- Table structure for table `stockrotationitems`
4256
--
4256
--
Lines 4269-4275 CREATE TABLE IF NOT EXISTS stockrotationitems ( Link Here
4269
      FOREIGN KEY (`stage_id`)
4269
      FOREIGN KEY (`stage_id`)
4270
      REFERENCES `stockrotationstages` (`stage_id`)
4270
      REFERENCES `stockrotationstages` (`stage_id`)
4271
      ON UPDATE CASCADE ON DELETE CASCADE
4271
      ON UPDATE CASCADE ON DELETE CASCADE
4272
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
4272
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
4273
4273
4274
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
4274
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
4275
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
4275
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
(-)a/installer/data/mysql/sysprefs.sql (-3 / +4 lines)
Lines 479-484 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
479
('reviewson','1','','If ON, enables patron reviews of bibliographic records in the OPAC','YesNo'),
479
('reviewson','1','','If ON, enables patron reviews of bibliographic records in the OPAC','YesNo'),
480
('RisExportAdditionalFields',  '', NULL ,  'Define additional RIS tags to export from MARC records in YAML format as an associative array with either a marc tag/subfield combination as the value, or a list of tag/subfield combinations.',  'textarea'),
480
('RisExportAdditionalFields',  '', NULL ,  'Define additional RIS tags to export from MARC records in YAML format as an associative array with either a marc tag/subfield combination as the value, or a list of tag/subfield combinations.',  'textarea'),
481
('RoutingListAddReserves','0','','If ON the patrons on routing lists are automatically added to holds on the issue.','YesNo'),
481
('RoutingListAddReserves','0','','If ON the patrons on routing lists are automatically added to holds on the issue.','YesNo'),
482
('RotationPreventTransfers','0',NULL,'If ON, prevent any transfers for items on stock rotation rotas, except for stock rotation transfers','YesNo'),
483
('RoutingListAddReserves','1','','If ON the patrons on routing lists are automatically added to holds on the issue.','YesNo'),
482
('RoutingListNote','To change this note edit <a href=\"/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=RoutingListNote#jumped\">RoutingListNote</a> system preference.','70|10','Define a note to be shown on all routing lists','Textarea'),
484
('RoutingListNote','To change this note edit <a href=\"/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=RoutingListNote#jumped\">RoutingListNote</a> system preference.','70|10','Define a note to be shown on all routing lists','Textarea'),
483
('RoutingSerials','1',NULL,'If ON, serials routing is enabled','YesNo'),
485
('RoutingSerials','1',NULL,'If ON, serials routing is enabled','YesNo'),
484
('SCOMainUserBlock','','70|10','Add a block of HTML that will display on the self checkout screen','Textarea'),
486
('SCOMainUserBlock','','70|10','Add a block of HTML that will display on the self checkout screen','Textarea'),
Lines 525-530 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
525
('StaffSerialIssueDisplayCount','3','','Number of serial issues to display per subscription in the Staff client','Integer'),
527
('StaffSerialIssueDisplayCount','3','','Number of serial issues to display per subscription in the Staff client','Integer'),
526
('StaticHoldsQueueWeight','0',NULL,'Specify a list of library location codes separated by commas -- the list of codes will be traversed and weighted with first values given higher weight for holds fulfillment -- alternatively, if RandomizeHoldsQueueWeight is set, the list will be randomly selective','Integer'),
528
('StaticHoldsQueueWeight','0',NULL,'Specify a list of library location codes separated by commas -- the list of codes will be traversed and weighted with first values given higher weight for holds fulfillment -- alternatively, if RandomizeHoldsQueueWeight is set, the list will be randomly selective','Integer'),
527
('StatisticsFields','location|itype|ccode', NULL, 'Define Fields (from the items table) used for statistics members','Free'),
529
('StatisticsFields','location|itype|ccode', NULL, 'Define Fields (from the items table) used for statistics members','Free'),
530
('StockRotation','0',NULL,'If ON, enables the stock rotation module','YesNo'),
528
('StoreLastBorrower','0','','If ON, the last borrower to return an item will be stored in items.last_returned_by','YesNo'),
531
('StoreLastBorrower','0','','If ON, the last borrower to return an item will be stored in items.last_returned_by','YesNo'),
529
('SubfieldsToAllowForRestrictedBatchmod','','Define a list of subfields for which edition is authorized when items_batchmod_restricted permission is enabled, separated by spaces. Example: 995\$f 995\$h 995\$j',NULL,'Free'),
532
('SubfieldsToAllowForRestrictedBatchmod','','Define a list of subfields for which edition is authorized when items_batchmod_restricted permission is enabled, separated by spaces. Example: 995\$f 995\$h 995\$j',NULL,'Free'),
530
('SubfieldsToAllowForRestrictedEditing','','Define a list of subfields for which edition is authorized when edit_items_restricted permission is enabled, separated by spaces. Example: 995\$f 995\$h 995\$j',NULL,'Free'),
533
('SubfieldsToAllowForRestrictedEditing','','Define a list of subfields for which edition is authorized when edit_items_restricted permission is enabled, separated by spaces. Example: 995\$f 995\$h 995\$j',NULL,'Free'),
Lines 617-623 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
617
('XSLTListsDisplay','default','','Enable XSLT stylesheet control over lists pages display on intranet','Free'),
620
('XSLTListsDisplay','default','','Enable XSLT stylesheet control over lists pages display on intranet','Free'),
618
('XSLTResultsDisplay','default','','Enable XSL stylesheet control over results page display on intranet','Free'),
621
('XSLTResultsDisplay','default','','Enable XSL stylesheet control over results page display on intranet','Free'),
619
('z3950AuthorAuthFields','701,702,700',NULL,'Define the MARC biblio fields for Personal Name Authorities to fill biblio.author','free'),
622
('z3950AuthorAuthFields','701,702,700',NULL,'Define the MARC biblio fields for Personal Name Authorities to fill biblio.author','free'),
620
('z3950NormalizeAuthor','0','','If ON, Personal Name Authorities will replace authors in biblio.author','YesNo'),
623
('z3950NormalizeAuthor','0','','If ON, Personal Name Authorities will replace authors in biblio.author','YesNo')
621
('StockRotation','0',NULL,'If ON, enables the stock rotation module','YesNo'),
622
('RotationPreventTransfers','0',NULL,'If ON, prevent any transfers for items on stock rotation rotas, except for stock rotation transfers','YesNo')
623
;
624
;
(-)a/koha-tmpl/intranet-tmpl/prog/css/staff-global.css (+128 lines)
Lines 3365-3367 span.name { Link Here
3365
    border-top: 1px solid #E8E8E8;
3365
    border-top: 1px solid #E8E8E8;
3366
    padding: 7px 0;
3366
    padding: 7px 0;
3367
}
3367
}
3368
3369
#stockrotation h3 {
3370
    margin: 30px 0 10px 0;
3371
}
3372
3373
#stockrotation .dialog h3 {
3374
    margin: 10px 0;
3375
}
3376
3377
#stockrotation .dialog {
3378
    margin-bottom: 20px;
3379
}
3380
3381
#stockrotation .highlight_stage,
3382
#catalog_stockrotation .highlight_stage {
3383
    font-weight: bold;
3384
}
3385
3386
#stockrotation #rota_form textarea {
3387
    width: 300px;
3388
    height: 100px;
3389
}
3390
3391
#stockrotation #rota_form #name {
3392
    width: 300px;
3393
}
3394
3395
#stockrotation #rota_form fieldset,
3396
#stockrotation #stage_form fieldset,
3397
#stockrotation #add_rota_item_form fieldset {
3398
    width: auto;
3399
}
3400
3401
#stockrotation .dialog.alert ul {
3402
    margin: 20px 0;
3403
}
3404
3405
#stockrotation .dialog.alert li {
3406
    list-style-type: none;
3407
}
3408
3409
#catalog_stockrotation .item_select_rota {
3410
    vertical-align: middle;
3411
}
3412
3413
#catalog_stockrotation h1 {
3414
    margin-bottom: 20px;
3415
}
3416
3417
#stockrotation td.actions,
3418
#catalog_stockrotation td.actions {
3419
    vertical-align: middle;
3420
}
3421
3422
#stockrotation .stage,
3423
#catalog_stockrotation .stage {
3424
    display: inline-block;
3425
    padding: 5px 7px;
3426
    margin: 3px 0 3px 0;
3427
    border-radius: 5px;
3428
    background-color: rgba(0,0,0,0.1);
3429
}
3430
3431
#stage_list_headings {
3432
    font-weight: bold;
3433
}
3434
3435
#stage_list_headings span {
3436
    padding: 3px;
3437
}
3438
3439
#manage_stages ul {
3440
    padding-left: 0;
3441
}
3442
3443
#manage_stages li {
3444
    list-style: none;
3445
    margin-bottom: 5px;
3446
}
3447
3448
#manage_stages li span {
3449
    padding: 6px 3px;
3450
}
3451
3452
#manage_stages .stagename {
3453
    width: 15em;
3454
    display: inline-block;
3455
}
3456
3457
#manage_stages .stageduration {
3458
    width: 10em;
3459
    display: inline-block;
3460
}
3461
3462
#manage_stages .stageactions {
3463
    display: inline-block;
3464
}
3465
3466
#manage_stages li:nth-child(odd) {
3467
    background-color : #F3F3F3;
3468
}
3469
3470
#manage_stages .drag_handle {
3471
    margin-right: 6px;
3472
    cursor: move;
3473
}
3474
3475
#manage_stages .drag_placeholder {
3476
    height: 2em;
3477
    border: 1px dotted #aaa;
3478
}
3479
3480
#manage_stages h3 {
3481
    display: inline-block;
3482
}
3483
3484
#manage_stages #ajax_status {
3485
    display: inline-block;
3486
    border: 1px solid #bcbcbc;
3487
    border-radius: 5px;
3488
    padding: 5px;
3489
    margin-left: 10px;
3490
    background: #f3f3f3;
3491
}
3492
3493
#manage_stages #manage_stages_help {
3494
    margin: 20px 0;
3495
}
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/biblio-view-menu.inc (+1 lines)
Lines 40-45 Link Here
40
[% IF ( issuehistoryview ) %]<li class="active">[% ELSE %]<li>[% END %]
40
[% IF ( issuehistoryview ) %]<li class="active">[% ELSE %]<li>[% END %]
41
<a href="/cgi-bin/koha/catalogue/issuehistory.pl?biblionumber=[% biblio_object_id | url  %]" >Checkout history</a></li>
41
<a href="/cgi-bin/koha/catalogue/issuehistory.pl?biblionumber=[% biblio_object_id | url  %]" >Checkout history</a></li>
42
[% IF ( CAN_user_tools_view_system_logs ) %][% IF ( logview ) %]<li class="active">[% ELSE %]<li>[% END %]<a href="/cgi-bin/koha/tools/viewlog.pl?do_it=1&amp;modules=CATALOGUING&amp;action=MODIFY&amp;object=[% biblio_object_id | url  %]">Modification log</a> </li>[% END %]
42
[% IF ( CAN_user_tools_view_system_logs ) %][% IF ( logview ) %]<li class="active">[% ELSE %]<li>[% END %]<a href="/cgi-bin/koha/tools/viewlog.pl?do_it=1&amp;modules=CATALOGUING&amp;action=MODIFY&amp;object=[% biblio_object_id | url  %]">Modification log</a> </li>[% END %]
43
[% IF ( CAN_user_stockrotation_manage_rota_items && Koha.Preference('StockRotation') ) %][% IF ( stockrotationview ) %]<li class="active">[% ELSE %]<li>[% END %]<a href="/cgi-bin/koha/catalogue/stockrotation.pl?biblionumber=[% biblio_object_id %]">Rota</a> </li>[% END %]
43
</ul>
44
</ul>
44
</div>
45
</div>
45
46
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc (-3 / +2 lines)
Lines 118-125 Link Here
118
  [%# self_check %]
118
  [%# self_check %]
119
    [%- CASE 'self_checkin_module' -%]<span>Log into the self check-in module. Note: this permission prevents the patron from using any other OPAC functionality</span>
119
    [%- CASE 'self_checkin_module' -%]<span>Log into the self check-in module. Note: this permission prevents the patron from using any other OPAC functionality</span>
120
    [%- CASE 'self_checkout_module' -%]<span>Perform self checkout at the OPAC. It should be used for the patron matching the AutoSelfCheckID</span>
120
    [%- CASE 'self_checkout_module' -%]<span>Perform self checkout at the OPAC. It should be used for the patron matching the AutoSelfCheckID</span>
121
    [%- CASE 'manage_rota_items' -%]<span>Add and remove items from rotas</span>
122
    [%- CASE 'manage_rotas' -%]<span>Create, edit and delete rotas</span>
121
  [%- END -%]
123
  [%- END -%]
122
    [%- CASE 'can_add_items_rotas' -%]<span>Add and remove items from rotas</span>
123
    [%- CASE 'can_edit_rotas' -%]<span>Create, edit and delete rotas</span>
124
    [%- END -%]
125
[%- END -%]
124
[%- END -%]
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/stockrotation-toolbar.inc (+12 lines)
Line 0 Link Here
1
[% USE Koha %]
2
<div id="toolbar" class="btn-toolbar">
3
    [% IF no_op_set %]
4
        <a id="addrota" class="btn btn-default btn-sm" href="/cgi-bin/koha/tools/stockrotation.pl?op=create_edit_rota"><i class="fa fa-plus"></i> New rota</a>
5
    [% END %]
6
    [% IF op == 'manage_stages' %]
7
        <a id="editrota" class="btn btn-default btn-sm" href="/cgi-bin/koha/tools/stockrotation.pl?op=create_edit_rota&amp;rota_id=[% rota_id %]"><i class="fa fa-pencil"></i> Edit rota</a>
8
    [% END %]
9
    [% IF op == 'manage_items' %]
10
        <a id="editrota" class="btn btn-default btn-sm" href="/cgi-bin/koha/tools/stockrotation.pl?op=create_edit_rota&amp;rota_id=[% rota_id %]"><i class="fa fa-pencil"></i> Edit rota</a>
11
    [% END %]
12
</div>
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc (+5 lines)
Lines 1-3 Link Here
1
[% USE Koha %]
2
1
<div id="navmenu">
3
<div id="navmenu">
2
<div id="navmenulist">
4
<div id="navmenulist">
3
<ul>
5
<ul>
Lines 38-43 Link Here
38
    [% IF ( CAN_user_tools_batch_upload_patron_images ) %]
40
    [% IF ( CAN_user_tools_batch_upload_patron_images ) %]
39
	<li><a href="/cgi-bin/koha/tools/picture-upload.pl">Upload patron images</a></li>
41
	<li><a href="/cgi-bin/koha/tools/picture-upload.pl">Upload patron images</a></li>
40
    [% END %]
42
    [% END %]
43
    [% IF ( CAN_user_stockrotation_manage_rotas && Koha.Preference('StockRotation') ) %]
44
    <li><a href="/cgi-bin/koha/tools/stockrotation.pl">Stock rotation</a></li>
45
    [% END %]
41
</ul>
46
</ul>
42
<h5>Catalog</h5>
47
<h5>Catalog</h5>
43
<ul>
48
<ul>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/stockrotation.tt (+171 lines)
Line 0 Link Here
1
[% USE Koha %]
2
[% USE Branches %]
3
[% INCLUDE 'doc-head-open.inc' %]
4
<title>Koha &rsaquo; Catalog &rsaquo; Stock rotation details for [% biblio.title %]</title>
5
[% INCLUDE 'doc-head-close.inc' %]
6
[% INCLUDE 'browser-strings.inc' %]
7
[% Asset.js("js/browser.js") %]
8
<script type="text/javascript">
9
//<![CDATA[
10
    var browser = KOHA.browser('[% searchid %]', parseInt('[% biblionumber %]', 10));
11
    browser.show();
12
//]]>
13
</script>
14
</head>
15
<body id="catalog_stockrotation" class="catalog">
16
[% USE KohaDates %]
17
[% INCLUDE 'header.inc' %]
18
[% INCLUDE 'cat-search.inc' %]
19
20
<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; <a href="/cgi-bin/koha/catalogue/search.pl">Catalog</a>  &rsaquo; Stock rotation details for <i>[% biblio.title | html %][% FOREACH subtitle IN biblio.subtitles %][% subtitle.subfield %][% END %]</i></div>
21
22
<div id="doc3" class="yui-t2">
23
24
   <div id="bd">
25
    <div id="yui-main">
26
    <div class="yui-b">
27
28
<div id="catalogue_detail_biblio">
29
30
    [% IF no_op_set %]
31
        <h1 class="title">Stock rotation details for [% biblio.title | html %]</h1>
32
        [% IF rotas.count > 0 && items.size > 0 %]
33
34
            <table class="items_table dataTable no-footer" role="grid">
35
                <thead>
36
                    <tr>
37
                        <th>Barcode</th>
38
                        <th>Callnumber</th>
39
                        <th>Rota</th>
40
                        <th>Rota status</th>
41
                        <th>In transit</th>
42
                        <th>Stages &amp; duration in days<br>(current stage highlighted)</th>
43
                        <th>&nbsp;</th>
44
                    </tr>
45
                </thead>
46
                <tbody>
47
                    [% FOREACH item IN items %]
48
                        <tr>
49
                            <td>[% item.bib_item.barcode %]</td>
50
                            <td>[% item.bib_item.itemcallnumber %]</td>
51
                            <td>
52
                                [% item.rota.title %]
53
                            </td>
54
                            <td>
55
                                [% IF item.rota %]
56
                                    [% IF !item.rota.active %]
57
                                        <span class="highlighted-row">
58
                                    [% END %]
59
                                        [% IF item.rota.active %]
60
                                            Active
61
                                        [% ELSE %]
62
                                            Inactive
63
                                        [% END %]
64
                                    [% IF !item.rota.active %]
65
                                        </span>
66
                                    [% END %]
67
                                [% END %]
68
                            </td>
69
                            <td>
70
                                [% IF item.bib_item.get_transfer %]
71
                                    Yes
72
                                [% ELSE %]
73
                                    No
74
                                [% END %]
75
                            </td>
76
                            <td>
77
                                [% FOREACH this_stage IN item.stages %]
78
                                    [% IF this_stage.stage_id == item.stockrotationitem.stage.stage_id %]
79
                                        <span class="stage highlight_stage">
80
                                    [% ELSE %]
81
                                        <span class="stage">
82
                                    [% END %]
83
                                    [% Branches.GetName(this_stage.branchcode_id) %] ([% this_stage.duration %])
84
                                    </span>
85
                                    &raquo;
86
                                [% END %]
87
                                [% IF item.stages.size > 0 %]
88
                                    <span class="stage">
89
                                        [% IF item.rota.cyclical %]
90
                                            START
91
                                        [% ELSE %]
92
                                            END
93
                                        [% END %]
94
                                    </span>
95
                                [% END %]
96
                            </td>
97
                            <td class="actions">
98
                                [% IF item.stockrotationitem %]
99
                                    [% in_transit = item.bib_item.get_transfer %]
100
                                    [% IF !in_transit && item.stages.size > 1 %]
101
                                        <a class="btn btn-default btn-xs" href="?op=move_to_next_stage&amp;stage_id=[% item.stockrotationitem.stage.stage_id %]&amp;item_id=[% item.bib_item.id %]&amp;biblionumber=[% biblionumber %]">
102
                                    [% ELSE %]
103
                                        <a class="btn btn-default btn-xs" disabled>
104
                                    [% END %]
105
                                        <i class="fa fa-arrow-right"></i>
106
                                        Move to next stage
107
                                    </a>
108
                                    [% IF !in_transit %]
109
                                        <a class="btn btn-default btn-xs" href="?op=toggle_in_demand&amp;stage_id=[% item.stockrotationitem.stage.stage_id %]&amp;item_id=[% item.bib_item.id %]&amp;biblionumber=[% biblionumber %]">
110
                                    [% ELSE %]
111
                                        <a class="btn btn-default btn-xs" disabled>
112
                                    [% END %]
113
                                        <i class="fa fa-fire"></i>
114
                                        [% IF item.stockrotationitem.indemand %]
115
                                            Remove "In demand"
116
                                        [% ELSE %]
117
                                            Add "In demand"
118
                                        [% END %]
119
                                    </a>
120
                                    [% IF !in_transit %]
121
                                        <a class="btn btn-default btn-xs" href="?op=confirm_remove_from_rota&amp;stage_id=[% item.stockrotationitem.stage.stage_id %]&amp;item_id=[% item.bib_item.id %]&amp;biblionumber=[% biblionumber %]">
122
                                    [% ELSE %]
123
                                        <a class="btn btn-default btn-xs" disabled>
124
                                    [% END %]
125
                                        <i class="fa fa-trash"></i>
126
                                        Remove from rota
127
                                    </a>
128
                                [% ELSE %]
129
                                    <form class="rota_select_form" method="post" enctype="multipart/form-data">
130
                                        <select class="item_select_rota" name="rota_id">
131
                                            [% FOREACH rota IN rotas %]
132
                                                <option value="[% rota.rota_id %]">[% rota.title %]</option>
133
                                            [% END %]
134
                                        </select>
135
                                        <button class="btn btn-default btn-xs" type="submit"><i class="fa fa-plus"></i> Add to rota</button>
136
                                        <input type="hidden" name="op" value="add_item_to_rota"></input>
137
                                        <input type="hidden" name="item_id" value="[% item.bib_item.id %]"></input>
138
                                        <input type="hidden" name="biblionumber" value="[% biblionumber %]"></input>
139
                                    </form>
140
                                [% END %]
141
                            </td>
142
                        </tr>
143
                    [% END %]
144
                </tbody>
145
            </table>
146
        [% END %]
147
        [% IF !items || items.size == 0 %]
148
            <h1>No physical items for this record</h1>
149
        [% END %]
150
        [% IF !rotas || rotas.count == 0 %]
151
            <h1>There are no rotas with stages assigned</h1>
152
        [% END %]
153
    [% ELSIF op == 'confirm_remove_from_rota' %]
154
        <div class="dialog alert">
155
            <h3>Are you sure you want to remove this item from it's rota?</h3>
156
            <p>
157
                <a class="btn btn-default btn-xs approve" href="?op=remove_item_from_stage&amp;stage_id=[% stage_id %]&amp;item_id=[% item_id %]&amp;biblionumber=[% biblionumber %]"><i class="fa fa-fw fa-check"></i>Yes</a>
158
                <a class="btn btn-default btn-xs deny" href="?biblionumber=[% biblionumber %]"><i class="fa fa-fw fa-remove"></i>No</a>
159
            </p>
160
        </div>
161
    [% END %]
162
163
</div>
164
165
</div>
166
</div>
167
<div class="yui-b">
168
[% INCLUDE 'biblio-view-menu.inc' %]
169
</div>
170
</div>
171
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stockrotation.tt (+510 lines)
Line 0 Link Here
1
[% USE Koha %]
2
[% USE Branches %]
3
[% USE KohaDates %]
4
[% INCLUDE 'doc-head-open.inc' %]
5
<title>Koha &rsaquo; Stock rotation</title>
6
[% INCLUDE 'doc-head-close.inc' %]
7
[% Asset.css("css/datatables.css") %]
8
[% INCLUDE 'datatables.inc' %]
9
[% Asset.js("js/pages/stockrotation.js") %]
10
<script type="text/javascript">
11
//<![CDATA[
12
    $(document).ready(function() {
13
        $('#sr_manage_items').dataTable($.extend(true, {}, dataTablesDefaults, {
14
            "autoWidth": false,
15
            "aoColumnDefs": [
16
                { "bSortable": false, "bSearchable": false, 'aTargets': [ 'NoSort' ] },
17
                { "bSortable": true, "bSearchable": false, 'aTargets': [ 'NoSearch' ] }
18
            ],
19
            "sPaginationType": "four_button"
20
        }));
21
    });
22
//]]>
23
</script>
24
</head>
25
26
<body>
27
[% INCLUDE 'header.inc' %]
28
[% INCLUDE 'patron-search.inc' %]
29
30
<div id="breadcrumbs">
31
    <a href="/cgi-bin/koha/mainpage.pl">Home</a>
32
    &rsaquo; <a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a>
33
34
[% IF no_op_set %]
35
    &rsaquo; Stock rotation
36
[% ELSE %]
37
    &rsaquo; <a href="/cgi-bin/koha/tools/stockrotation.pl">Stock rotation</a>
38
[% END %]
39
40
[% IF (op == 'create_edit_rota' && rota.rota_id) %]
41
    &rsaquo; Edit rota
42
[% ELSIF (op == 'create_edit_rota' && !rota.rota_id) %]
43
    &rsaquo; Create rota
44
[% ELSIF (op == 'manage_stages') %]
45
    &rsaquo; Manage stages
46
[% ELSIF (op == 'create_edit_stage' && stage.id) %]
47
    <a href="?op=manage_stages&amp;rota_id=[% rota_id %]">&rsaquo; Manage stages</a>
48
    &rsaquo; Edit stage
49
[% ELSIF (op == 'create_edit_stage' && !stage.id) %]
50
    <a href="?op=manage_stages&amp;rota_id=[% rota_id %]">&rsaquo; Manage stages</a>
51
    &rsaquo; Create stage
52
[% ELSIF (op == 'manage_items') %]
53
    &rsaquo; Manage items
54
[% END %]
55
56
</div>
57
58
<div id="doc3" class="yui-t2">
59
    <div id="bd">
60
        <div id="yui-main">
61
            <div id="stockrotation" class="yui-b">
62
63
                [% IF no_op_set %]
64
65
                    [% INCLUDE 'stockrotation-toolbar.inc' %]
66
67
                    <h2>Stock rotation</h2>
68
69
                    [% IF existing_rotas.size > 0 %]
70
                        <table class="rotas_table" role="grid">
71
                            <thead>
72
                                <th>Name</th>
73
                                <th>Cyclical</th>
74
                                <th>Active</th>
75
                                <th>Description</th>
76
                                <th>Number of items</th>
77
                                <th>&nbsp;</th>
78
                            </thead>
79
                            <tbody>
80
                                [% FOREACH rota IN existing_rotas %]
81
                                    <tr>
82
                                        <td>[% rota.title %]</td>
83
                                        <td>[% rota.cyclical ? 'Yes' : 'No'%]</td>
84
                                        <td>[% rota.active ? 'Yes' : 'No'%]</td>
85
                                        <td>[% rota.description %]</td>
86
                                        <td>[% rota.stockrotationitems.count %]</td>
87
                                        <td class="actions">
88
                                            <a class="btn btn-default btn-xs" href="?op=create_edit_rota&amp;rota_id=[% rota.rota_id %]">
89
                                                <i class="fa fa-pencil"></i>
90
                                                Edit
91
                                            </a>
92
                                            <div class="btn-group" role="group">
93
                                                <button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
94
                                                    Manage
95
                                                    <i class="fa fa-caret-down"></i>
96
                                                </button>
97
                                                <ul class="dropdown-menu">
98
                                                    <li><a href="?op=manage_stages&amp;rota_id=[% rota.rota_id %]">Stages</a></li>
99
                                                    [% IF CAN_user_stockrotation_manage_rota_items && rota.stockrotationstages.count > 0 %]
100
                                                    <li><a href="?op=manage_items&amp;rota_id=[% rota.rota_id %]">Items</a></li>
101
                                                    [% END %]
102
                                                </ul>
103
                                            </div>
104
                                            <a class="btn btn-default btn-xs" href="?op=toggle_rota&amp;rota_id=[% rota.rota_id %]">
105
                                                <i class="fa fa-power-off"></i>
106
                                                [% IF !rota.active %]
107
                                                    Activate
108
                                                [% ELSE %]
109
                                                    Deactivate
110
                                                [% END %]
111
                                            </a>
112
                                        </td>
113
                                    </tr>
114
                                [% END %]
115
                            </tbody>
116
                        </table>
117
                    [% END %]
118
119
                [% ELSIF (op == 'create_edit_rota') %]
120
121
                    [% IF rota.rota_id %]
122
                        <h2>Edit "[% rota.title %]"</h2>
123
                    [% ELSE %]
124
                        <h2>Create new rota</h2>
125
                    [% END %]
126
127
                    [% IF error == 'invalid_form' %]
128
                    <div class="dialog alert">
129
                        <h3>There was a problem with your form submission</h3>
130
                    </div>
131
                    [% END %]
132
133
                    <form id="rota_form" method="post" enctype="multipart/form-data" class="validated">
134
                        <fieldset class="rows">
135
                            <ol>
136
                                <li>
137
                                    <label class="required" for="title">Name:</label>
138
                                    <input type="text" id="title" name="title" value="[% rota.title %]" required="required" placeholder="Rota name">
139
                                    <span class="required">Required</span>
140
                                </li>
141
                                <li>
142
                                    <label for="cyclical">Cyclical:</label>
143
                                    <select name="cyclical" id="cyclical">
144
                                        [% IF rota.cyclical %]
145
                                            <option value="1" selected>Yes</option>
146
                                            <option value="0">No</option>
147
                                        [% ELSE %]
148
                                            <option value="1">Yes</option>
149
                                            <option value="0" selected>No</option>
150
                                        [% END %]
151
                                    </select>
152
                                </li>
153
                                <li>
154
                                    <label for="description">Description:</label>
155
                                    <textarea id="description" name="description" placeholder="Rota description">[% rota.description %]</textarea>
156
                                </li>
157
                            </ol>
158
                        </fieldset>
159
                        <fieldset class="action">
160
                            <input type="submit" value="Save">
161
                            <a href="/cgi-bin/koha/tools/stockrotation.pl" class="cancel">Cancel</a>
162
                        </fieldset>
163
                        [% IF rota.rota_id %]
164
                            <input type="hidden" name="id" value="[% rota.rota_id %]">
165
                        [% END %]
166
                        <input type="hidden" name="op" value="process_rota">
167
                    </form>
168
169
                [% ELSIF (op == 'manage_stages') %]
170
171
                    [% INCLUDE 'stockrotation-toolbar.inc' %]
172
173
                    [% IF error == 'invalid_form' %]
174
                    <div class="dialog alert">
175
                        <h3>There was a problem with your form submission</h3>
176
                    </div>
177
                    [% END %]
178
179
                    <h2>Manage [% rota.title %] stages</h2>
180
                    <div id="ajax_status"
181
                        data-saving-msg="Saving changes..."
182
                        data-success-msg=""
183
                        data-failed-msg="Error: ">
184
                        <span id="ajax_saving_msg"></span>
185
                        <i id="ajax_saving_icon" class="fa fa-spinner fa-spin"></i>
186
                        <i id="ajax_success_icon" class="fa fa-check"></i>
187
                        <i id="ajax_failed_icon" class="fa fa-times"></i>
188
                        <span id="ajax_success_msg"></span>
189
                        <span id="ajax_failed_msg"></span>
190
                    </div>
191
192
                    <form id="stage_form" method="post" enctype="multipart/form-data" class="validated">
193
                        <fieldset class="rows">
194
                            <legend>Add stage</legend>
195
                            <ol>
196
                                <li>
197
                                    <label class="required" for="branch">Library:</label>
198
                                    <select name="branchcode" id="branch">
199
                                        [% FOREACH branch IN branches %]
200
                                            [% IF branch.branchcode == stage.branchcode_id %]
201
                                                <option value="[% branch.branchcode %]" selected>[% Branches.GetName(branch.branchcode) %]</option>
202
                                            [% ELSE %]
203
                                                <option value="[% branch.branchcode %]">[% Branches.GetName(branch.branchcode) %]</option>
204
                                            [% END %]
205
                                        [% END %]
206
                                    </select>
207
                                    <span class="required">Required</span>
208
                                </li>
209
                                <li>
210
                                    <label class="required" for="duration">Duration:</label>
211
                                    <input type="text" id="duration" name="duration" value="[% stage.duration %]" required="required" placeholder="Duration (days)">
212
                                    <span class="required">Required</span>
213
                                </li>
214
                            </ol>
215
                        </fieldset>
216
                        <fieldset class="action">
217
                            <input type="submit" value="Submit">
218
                        </fieldset>
219
                        <input type="hidden" name="stage_id" value="[% stage.id %]">
220
                        <input type="hidden" name="rota_id" value="[% rota_id %]">
221
                        <input type="hidden" name="op" value="process_stage">
222
                    </form>
223
224
                    [% IF existing_stages.size > 0 %]
225
                        <div id="manage_stages">
226
                            <div id="manage_stages_help">
227
                                Stages can be re-ordered by using the <i class="drag_handle fa fa-lg fa-bars"></i>handle to drag and drop them to their new position
228
                            </div>
229
                            <div id="stage_list_headings">
230
                                <span class="stagename">Library</span>
231
                                <span class="stageduration">Duration (days)</span>
232
                            </div>
233
                            <ul id="sortable_stages" data-rota-id="[% rota.rota_id %]">
234
                                [% FOREACH stage IN existing_stages %]
235
                                    <li id="stage_[% stage.stage_id %]">
236
                                        <span data-toggle="tooltip" title="Drag and drop to move this stage to another position" data-placement="right" class="stagename">
237
                                            [% IF existing_stages.size > 1 %]
238
                                                <i class="drag_handle fa fa-lg fa-bars"></i>
239
                                            [% END %]
240
                                            [% Branches.GetName(stage.branchcode_id) %]
241
                                        </span>
242
                                        <span class="stageduration">[% stage.duration %]</span>
243
                                        <span class="stageactions">
244
                                            <a class="btn btn-default btn-xs" href="?op=create_edit_stage&amp;stage_id=[% stage.stage_id %]">
245
                                                <i class="fa fa-pencil"></i> Edit
246
                                            </a>
247
                                            <a class="btn btn-default btn-xs" href="?op=confirm_delete_stage&amp;stage_id=[% stage.stage_id %]">
248
                                                <i class="fa fa-trash"></i> Delete
249
                                            </a>
250
                                        </span>
251
                                    </li>
252
                                [% END %]
253
                            </ul>
254
                        </div>
255
                    [% END %]
256
257
                    <p><a href="stockrotation.pl">Return to rotas</a></p>
258
259
                [% ELSIF (op == 'create_edit_stage') %]
260
261
                    [% IF stage.id %]
262
                        <h2>Edit "[% Branches.GetName(stage.branchcode_id) %]"</h2>
263
                    [% ELSE %]
264
                        <h2>Create new stage</h2>
265
                    [% END %]
266
267
                    [% IF error == 'invalid_form' %]
268
                    <div class="dialog alert">
269
                        <h3>There was a problem with your form submission</h3>
270
                    </div>
271
                    [% END %]
272
273
                    <form id="stage_form" method="post" enctype="multipart/form-data" class="validated">
274
                        <fieldset class="rows">
275
                            <ol>
276
                                <li>
277
                                    <label class="required" for="branch">Library:</label>
278
                                    <select name="branchcode" id="branch">
279
                                        [% FOREACH branch IN branches %]
280
                                            [% IF branch.branchcode == stage.branchcode_id %]
281
                                                <option value="[% branch.branchcode %]" selected>[% Branches.GetName(branch.branchcode) %]</option>
282
                                            [% ELSE %]
283
                                                <option value="[% branch.branchcode %]">[% Branches.GetName(branch.branchcode) %]</option>
284
                                            [% END %]
285
                                        [% END %]
286
                                    </select>
287
                                    <span class="required">Required</span>
288
                                </li>
289
                                <li>
290
                                    <label class="required" for="duration">Duration:</label>
291
                                    <input type="text" id="duration" name="duration" value="[% stage.duration %]" required="required" placeholder="Duration (days)">
292
                                    <span class="required">Required</span>
293
                                </li>
294
                            </ol>
295
                        </fieldset>
296
                        <fieldset class="action">
297
                            <input type="submit" value="Save">
298
                            <a href="/cgi-bin/koha/tools/stockrotation.pl?op=manage_stages&amp;rota_id=[% rota_id %]" class="cancel">Cancel</a>
299
                        </fieldset>
300
                        <input type="hidden" name="stage_id" value="[% stage.id %]">
301
                        <input type="hidden" name="rota_id" value="[% rota_id %]">
302
                        <input type="hidden" name="op" value="process_stage">
303
                    </form>
304
                [% ELSIF (op == 'confirm_remove_from_rota') %]
305
306
                    <div class="dialog alert">
307
                        <h3>Are you sure you wish to remove this item from it's rota</h3>
308
                        <p>
309
                            <a class="btn btn-default btn-xs approve" href="?op=remove_item_from_stage&amp;item_id=[% item_id %]&amp;stage_id=[% stage_id %]&amp;rota_id=[% rota_id %]"><i class="fa fa-fw fa-check"></i>Yes</a>
310
                            <a class="btn btn-default btn-xs deny" href="?op=manage_items&amp;rota_id=[% rota_id %]"><i class="fa fa-fw fa-remove"></i>No</a>
311
                        </p>
312
                    </div>
313
                [% ELSIF (op == 'confirm_delete_stage') %]
314
315
                    <div class="dialog alert">
316
                        <h3>Are you sure you want to delete this stage?</h3>
317
                        [% IF stage.stockrotationitems.count > 0 %]
318
                            <p>This stage contains the following item(s):</p>
319
                            <ul>
320
                                [% FOREACH item IN stage.stockrotationitems %]
321
                                    <li>[% item.itemnumber.biblio.title %] (Barcode: [% item.itemnumber.barcode %])</li>
322
                                [% END %]
323
                            </ul>
324
                        [% END %]
325
                        <p>
326
                            <a class="btn btn-default btn-xs approve" href="?op=delete_stage&amp;stage_id=[% stage.stage_id %]"><i class="fa fa-fw fa-check"></i>Yes</a>
327
                            <a class="btn btn-default btn-xs deny" href="?op=manage_stages&amp;rota_id=[% stage.rota.rota_id %]"><i class="fa fa-fw fa-remove"></i>No</a>
328
                        </p>
329
                    </div>
330
                [% ELSIF (op == 'manage_items') %]
331
332
                    [% INCLUDE 'stockrotation-toolbar.inc' %]
333
334
                    [% IF error %]
335
                        <div class="dialog alert">
336
                            [% IF error == "item_not_found" %]
337
                                <h3>The item was not found</h3>
338
                            [% ELSIF error == "already_on_rota" %]
339
                                <h3>This item is already on this rota</h3>
340
                            [% END %]
341
                        </div>
342
                    [% END %]
343
344
                    <h2>Manage [% rota.title %] items</h2>
345
346
                    <div>
347
                        <form id="add_rota_item_form" method="post" enctype="multipart/form-data" class="validated">
348
                            <fieldset class="rows">
349
                                <legend>Add item to &quot;[% rota.title %]&quot;</legend>
350
                                <ol>
351
                                    <li>
352
                                        <label for="barcode">Barcode:</label>
353
                                        <input type="text" id="barcode" name="barcode" placeholder="Item barcode" autofocus>
354
                                    </li>
355
                                </ol>
356
                            </fieldset>
357
                            <fieldset class="rows">
358
                                <legend>Use a barcode file</legend>
359
                                <ol>
360
                                    <li>
361
                                        <label for="barcodefile">Barcode file:</label>
362
                                        <input type="file" id="barcodefile" name="barcodefile">
363
                                    </li>
364
                                </ol>
365
                            </fieldset>
366
                            <fieldset class="action">
367
                                <input type="submit" value="Save">
368
                            </fieldset>
369
                            <input type="hidden" name="rota_id" value="[% rota.id %]">
370
                            <input type="hidden" name="op" value="add_items_to_rota">
371
                        </form>
372
                    </div>
373
374
                    [% IF items.count > 0 %]
375
                        <h3>Manage items assigned to &quot;[% rota.title %]&quot;</h3>
376
                        <table id="sr_manage_items" class="items_table" role="grid">
377
                            <thead>
378
                                <th>Barcode</th>
379
                                <th>Title</th>
380
                                <th>Author</th>
381
                                <th>Callnumber</th>
382
                                <th class="NoSearch">In transit</th>
383
                                <th class="NoSort">Stages &amp; duration in days<br>(current stage highlighted)</th>
384
                                <th class="NoSort">&nbsp;</th>
385
                            </thead>
386
                            <tbody>
387
                                [% FOREACH item IN items %]
388
                                    <tr>
389
                                        <td><a href="/cgi-bin/koha/catalogue/moredetail.pl?itemnumber=[% item.id %]&amp;biblionumber=[% item.itemnumber.biblio.id %]#item[% item.id %]">[% item.itemnumber.barcode %]</a></td>
390
                                        <td><a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% item.itemnumber.biblio.id %]">[% item.itemnumber.biblio.title %]</a></td>
391
                                        <td>[% item.itemnumber.biblio.author %]</td>
392
                                        <td>[% item.itemnumber.itemcallnumber %]</td>
393
                                        <td>[% item.itemnumber.get_transfer ? 'Yes' : 'No' %]</td>
394
                                        <td>
395
                                            [% FOREACH this_stage IN stages %]
396
                                                [% IF this_stage.stage_id == item.stage.stage_id %]
397
                                                    <span class="stage highlight_stage">
398
                                                [% ELSE %]
399
                                                    <span class="stage">
400
                                                [% END %]
401
                                                [% Branches.GetName(this_stage.branchcode_id) %] ([% this_stage.duration %])
402
                                                </span>
403
                                                &raquo;
404
                                            [% END %]
405
                                            [% IF stages.size > 0 %]
406
                                                <span class="stage">[% rota.cyclical ? 'START' : 'END' %]</span>
407
                                            [% END %]
408
                                        </td>
409
                                        <td class="actions">
410
                                            [% in_transit = item.itemnumber.get_transfer %]
411
                                            [% IF !in_transit && stages.size > 1 %]
412
                                                <a class="btn btn-default btn-xs" href="?op=move_to_next_stage&amp;rota_id=[% rota.id %]&amp;item_id=[% item.id %]&amp;stage_id=[% item.stage.stage_id %]">
413
                                            [% ELSE %]
414
                                                <a class="btn btn-default btn-xs" disabled>
415
                                            [% END %]
416
                                                <i class="fa fa-arrow-right"></i>
417
                                                Move to next stage
418
                                            </a>
419
                                            [% IF !in_transit %]
420
                                                <a class="btn btn-default btn-xs" href="?op=toggle_in_demand&amp;stage_id=[% item.stage.stage_id %]&amp;item_id=[% item.id %]&amp;rota_id=[% rota.id %]">
421
                                            [% ELSE %]
422
                                                <a class="btn btn-default btn-xs" disabled>
423
                                            [% END %]
424
                                                <i class="fa fa-fire"></i>
425
                                                [% item.indemand ? 'Remove &quot;In demand&quot;' : 'Add &quot;In demand&quot;' %]
426
                                            </a>
427
                                            [% IF !in_transit %]
428
                                                <a class="btn btn-default btn-xs" href="?op=confirm_remove_from_rota&amp;stage_id=[% item.stage.stage_id %]&amp;item_id=[% item.id %]&amp;rota_id=[% rota.id %]">
429
                                            [% ELSE %]
430
                                                <a class="btn btn-default btn-xs" disabled>
431
                                            [% END %]
432
                                                <i class="fa fa-trash"></i>
433
                                                Remove from rota
434
                                            </a>
435
                                        </td>
436
                                    </tr>
437
                                [% END %]
438
                            </tbody>
439
                        </table>
440
                    [% END %]
441
442
                    <p><a href="stockrotation.pl">Return to rotas</a></p>
443
444
                [% ELSIF op == 'add_items_to_rota' %]
445
446
                    <div class="dialog message">
447
                        <h3>Add items to rota report</h3>
448
                    </div>
449
450
                    <div>
451
                        [% IF barcode_status.ok.size > 0 %]
452
                            <h4>Items added to rota:</h4>
453
                            <ul>
454
                                [% FOREACH item_ok IN barcode_status.ok %]
455
                                    <li>[% item_ok.biblio.title %]</li>
456
                                [% END %]
457
                            </ul>
458
                        [% END %]
459
                        [% IF barcode_status.on_this.size > 0 %]
460
                            <h4>Items already on this rota:</h4>
461
                            <ul>
462
                                [% FOREACH item_on_this IN barcode_status.on_this %]
463
                                    <li>[% item_on_this.biblio.title %]</li>
464
                                [% END %]
465
                            </ul>
466
                        [% END %]
467
                        [% IF barcode_status.not_found.size > 0 %]
468
                            <h4>Barcodes not found:</h4>
469
                            <ul>
470
                                [% FOREACH barcode_not_found IN barcode_status.not_found %]
471
                                    <li>[% barcode_not_found %]</li>
472
                                [% END %]
473
                            </ul>
474
                        [% END %]
475
                        [% IF barcode_status.on_other.size > 0 %]
476
                            <h4>Items found on other rotas:</h4>
477
                            <ul>
478
                                [% FOREACH item_on_other IN barcode_status.on_other %]
479
                                    <li>[% item_on_other.biblio.title %]</li>
480
                                [% END %]
481
                            </ul>
482
                        [% END %]
483
                    </div>
484
                    [% IF barcode_status.on_other.size > 0 %]
485
                        <form id="add_rota_item_form" method="post" enctype="multipart/form-data">
486
                            <fieldset>
487
                                <legend>Select items to move to this rota:</legend>
488
                                [% FOREACH item_on_other IN barcode_status.on_other %]
489
                                    <li><input type="checkbox" name="move_item" value="[% item_on_other.itemnumber %]"> [% item_on_other.biblio.title %] (Currently on &quot;[% item_on_other.stockrotationitem.stage.rota.title %]&quot;)</li>
490
                                [% END %]
491
492
                            </fieldset>
493
                            <fieldset class="action">
494
                                <input type="submit" value="Save">
495
                            </fieldset>
496
                            <input type="hidden" name="rota_id" value="[% rota_id %]">
497
                            <input type="hidden" name="op" value="move_items_to_rota">
498
                        </form>
499
                    [% END %]
500
                    <p><a href="?op=manage_items&amp;rota_id=[% rota_id %]">Return to rota</a></p>
501
502
                [% END %]
503
            </div>
504
        </div>
505
        <div class="yui-b">
506
            [% INCLUDE 'tools-menu.inc' %]
507
        </div>
508
    </div>
509
</div>
510
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt (+7 lines)
Lines 1-3 Link Here
1
[% USE Koha %]
2
1
[% INCLUDE 'doc-head-open.inc' %]
3
[% INCLUDE 'doc-head-open.inc' %]
2
<title>Koha &rsaquo; Tools</title>
4
<title>Koha &rsaquo; Tools</title>
3
[% INCLUDE 'doc-head-close.inc' %]
5
[% INCLUDE 'doc-head-close.inc' %]
Lines 66-71 Link Here
66
    [% IF ( CAN_user_tools_batch_upload_patron_images ) %]
68
    [% IF ( CAN_user_tools_batch_upload_patron_images ) %]
67
    <dt><a href="/cgi-bin/koha/tools/picture-upload.pl">Upload patron images</a></dt>
69
    <dt><a href="/cgi-bin/koha/tools/picture-upload.pl">Upload patron images</a></dt>
68
	<dd>Upload patron images in a batch or one at a time</dd>
70
	<dd>Upload patron images in a batch or one at a time</dd>
71
    [% END %]
72
73
    [% IF ( CAN_user_stockrotation_manage_rotas && Koha.Preference('StockRotation') ) %]
74
    <dt><a href="/cgi-bin/koha/tools/stockrotation.pl">Stock rotation</a></dt>
75
    <dd>Manage Stock rotation rotas, rota stages and rota items</dd>
69
    [% END %]
76
    [% END %]
70
	</dl>
77
	</dl>
71
</div>
78
</div>
(-)a/koha-tmpl/intranet-tmpl/prog/js/pages/stockrotation.js (+65 lines)
Line 0 Link Here
1
function init() {
2
    $('#ajax_status').hide();
3
    $('#ajax_saving_msg').hide();
4
    $('#ajax_saving_icon').hide();
5
    $('#ajax_success_icon').hide();
6
    $('#ajax_failed_icon').hide();
7
    $('#ajax_failed_msg').hide();
8
}
9
10
$(document).ready(function() {
11
    var apiEndpoint = '/api/v1/rotas/';
12
    init();
13
    $('#sortable_stages').sortable({
14
        handle: '.drag_handle',
15
        placeholder: 'drag_placeholder',
16
        update: function(event, ui) {
17
            init();
18
            $('#sortable_stages').sortable('disable');
19
            var rotaId = document.getElementById('sortable_stages').dataset.rotaId;
20
            $('#ajax_saving_msg').text(
21
                document.getElementById('ajax_status').dataset.savingMsg
22
            );
23
            $('#ajax_saving_icon').show();
24
            $('#ajax_saving_msg').show();
25
            $('#ajax_status').fadeIn();
26
            var stageId = ui.item[0].id.replace(/^stage_/, '');
27
            var newIndex = ui.item.index();
28
            var newPosition = newIndex + 1;
29
            $.ajax({
30
                method: 'PUT',
31
                url: apiEndpoint + rotaId + '/stages/' + stageId + '/position',
32
                processData: false,
33
                contentType: 'application/json',
34
                data: newPosition
35
            })
36
            .done(function(data) {
37
                $('#ajax_success_msg').text(
38
                    document.getElementById('ajax_status').dataset.successMsg
39
                );
40
                $('#ajax_saving_icon').hide();
41
                $('#ajax_success_icon').show();
42
                $('#ajax_success_msg').show();
43
                setTimeout(
44
                    function() {
45
                        $('#ajax_status').fadeOut();
46
                    },
47
                    700
48
                );
49
            })
50
            .fail(function(jqXHR, status, error) {
51
                $('#ajax_failed_msg').text(
52
                    document.getElementById('ajax_status').dataset.failedMsg +
53
                    error
54
                );
55
                $('#ajax_saving_icon').hide();
56
                $('#ajax_failed_icon').show();
57
                $('#ajax_failed_msg').show();
58
                $('#sortable_stages').sortable('cancel');
59
            })
60
            .always(function() {
61
                $('#sortable_stages').sortable('enable');
62
            })
63
        }
64
    });
65
});
(-)a/misc/cronjobs/stockrotation.pl (+528 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# Copyright 2016 PTFS Europe
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
=head1 NAME
21
22
stockrotation.pl
23
24
=head1 SYNOPSIS
25
26
    --[a]dmin-email    An address to which email reports should also be sent
27
    --[b]ranchcode     Select branch to report on for 'email' reports (default: all)
28
    --e[x]ecute        Actually perform stockrotation housekeeping
29
    --[r]eport         Select either 'full' or 'email'
30
    --[S]end-all       Send email reports even if the report body is empty
31
    --[s]end-email     Send reports by email
32
    --[h]elp           Display this help message
33
34
Cron script implementing scheduled stockrotation functionality.
35
36
By default this script merely reports on the current status of the
37
stockrotation subsystem.  In order to actually place items in transit, the
38
script must be run with the `execute` argument.
39
40
`report` allows you to select the type of report that will be emitted. It's
41
set to 'full' by default.  If the `email` report is selected, you can use the
42
`branchcode` parameter to specify which branch's report you would like to see.
43
The default is 'all'.
44
45
`admin-email` is an additional email address to which we will send all email
46
reports in addition to sending them to branch email addresses.
47
48
`send-email` will cause the script to send reports by email, and `send-all`
49
will cause even reports with an empty body to be sent.
50
51
=head1 DESCRIPTION
52
53
This script is used to move items from one stockrotationstage to the next,
54
if they are elible for processing.
55
56
it should be run from cron like:
57
58
   stockrotation.pl --report email --send-email --execute
59
60
Prior to that you can run the script from the command line without the
61
--execute and --send-email parameters to see what reports the script would
62
generate in 'production' mode.  This is immensely useful for testing, or for
63
getting to understand how the stockrotation module works: you can set up
64
different scenarios, and then "query" the system on what it would do.
65
66
Normally you would want to run this script once per day, probably around
67
midnight-ish to move any stockrotationitems along their rotas and to generate
68
the email reports for branch libraries.
69
70
Each library will receive a report with "items of interest" for them for
71
today's rota checks.  Each item there will be an item that should, according
72
to Koha, be located on the shelves of that branch, and which should be picked
73
up and checked in.  The item will either:
74
- have been placed in transit to their new stage library;
75
- have been placed in transit to be returned to their current stage library;
76
- have just been added to a rota and will already be at the correct library;
77
78
In the last case the item will be checked in and no message will pop up.  In
79
the other cases a message will pop up requesting the item be posted to their
80
new branch.
81
82
=head2 What does the --execute flag do?
83
84
To understand this, you will need to know a little bit about the design of
85
this script and the stockrotation modules.
86
87
This script operates in 3 phases: first it walks the graph of rotas, stages
88
and items.  For each active rota, it investigates the items in each stage and
89
determines whether action is required.  It does not perform any actions, it
90
just "sieves" all items on active rotas into "actionable" and "non-actionable"
91
baskets.  We can use these baskets to perform actions against the items, or to
92
generate reports.
93
94
During the second phase this script then loops through the actionable baskets,
95
and performs the relevant action (initiate, repatriate, advance) on each item.
96
97
Finally, during the third phase we revisit the original baskets and we compile
98
reports (for instance per branch email reports).
99
100
When the script is run without the "--execute" flag, we perform phase 1, skip
101
phase 2 and move straight onto phase 3.
102
103
With the "--execute" flag we also perform the database operations.
104
105
So with or without the flag, the report will look the same (except for the "No
106
database updates have been performed.").
107
108
=cut
109
110
use Modern::Perl;
111
use Getopt::Long qw/HelpMessage :config gnu_getopt/;
112
use C4::Context;
113
use C4::Letters;
114
use Koha::StockRotationRotas;
115
116
my $admin_email = '';
117
my $branch      = 0;
118
my $execute     = 0;
119
my $report      = 'full';
120
my $send_all    = 0;
121
my $send_email  = 0;
122
123
my $ok = GetOptions(
124
    'admin-email|a=s' => \$admin_email,
125
    'branchcode|b=s'  => sub {
126
        my ( $opt_name, $opt_value ) = @_;
127
        my $branches = Koha::Libraries->search( {},
128
            { order_by => { -asc => 'branchname' } } );
129
        my $brnch = $branches->find($opt_value);
130
        if ($brnch) {
131
            $branch = $brnch;
132
            return $brnch;
133
        }
134
        else {
135
            printf("Option $opt_name should be one of (name -> code):\n");
136
            while ( my $candidate = $branches->next ) {
137
                printf( "  %-40s  ->  %s\n",
138
                    $candidate->branchname, $candidate->branchcode );
139
            }
140
            exit 1;
141
        }
142
    },
143
    'execute|x'  => \$execute,
144
    'report|r=s' => sub {
145
        my ( $opt_name, $opt_value ) = @_;
146
        if ( $opt_value eq 'full' || $opt_value eq 'email' ) {
147
            $report = $opt_value;
148
        }
149
        else {
150
            printf("Option $opt_name should be either 'email' or 'full'.\n");
151
            exit 1;
152
        }
153
    },
154
    'send-all|S'   => \$send_all,
155
    'send-email|s' => \$send_email,
156
    'help|h|?'     => sub { HelpMessage }
157
);
158
exit 1 unless ($ok);
159
160
$send_email++ if ($send_all);    # if we send all, then we must want emails.
161
162
=head2 Helpers
163
164
=head3 execute
165
166
  undef = execute($report);
167
168
Perform the database updates, within a transaction, that are reported as
169
needing to be performed by $REPORT.
170
171
$REPORT should be the return value of an invocation of `investigate`.
172
173
This procedure WILL mess with your database.
174
175
=cut
176
177
sub execute {
178
    my ($data) = @_;
179
180
    # Begin transaction
181
    my $schema = Koha::Database->new->schema;
182
    $schema->storage->txn_begin;
183
184
    # Carry out db updates
185
    foreach my $item ( @{ $data->{items} } ) {
186
        my $reason = $item->{reason};
187
        if ( $reason eq 'repatriation' ) {
188
            $item->{object}->repatriate;
189
        }
190
        elsif ( grep { $reason eq $_ } qw/in-demand advancement initiation/ ) {
191
            $item->{object}->advance;
192
        }
193
    }
194
195
    # End transaction
196
    $schema->storage->txn_commit;
197
}
198
199
=head3 report_full
200
201
  my $full_report = report_full($report);
202
203
Return an arrayref containing a string containing a detailed report about the
204
current state of the stockrotation subsystem.
205
206
$REPORT should be the return value of `investigate`.
207
208
No data in the database is manipulated by this procedure.
209
210
=cut
211
212
sub report_full {
213
    my ($data) = @_;
214
215
    my $header = "";
216
    my $body   = "";
217
218
    # Summary
219
    $header .= sprintf "
220
STOCKROTATION REPORT
221
--------------------\n";
222
    $body .= sprintf "
223
  Total number of rotas:         %5u
224
    Inactive rotas:              %5u
225
    Active rotas:                %5u
226
  Total number of items:         %5u
227
    Inactive items:              %5u
228
    Stationary items:            %5u
229
    Actionable items:            %5u
230
  Total items to be initiated:   %5u
231
  Total items to be repatriated: %5u
232
  Total items to be advanced:    %5u
233
  Total items in demand:         %5u\n\n",
234
      $data->{sum_rotas},  $data->{rotas_inactive}, $data->{rotas_active},
235
      $data->{sum_items},  $data->{items_inactive}, $data->{stationary},
236
      $data->{actionable}, $data->{initiable},      $data->{repatriable},
237
      $data->{advanceable}, $data->{indemand};
238
239
    if ( @{ $data->{rotas} } ) {    # Per Rota details
240
        $body .= sprintf "ROTAS DETAIL\n------------\n\n";
241
        foreach my $rota ( @{ $data->{rotas} } ) {
242
            $body .= sprintf "Details for %s [%s]:\n",
243
              $rota->{name}, $rota->{id};
244
            $body .= sprintf "\n  Items:";    # Rota item details
245
            if ( @{ $rota->{items} } ) {
246
                $body .=
247
                  join( "", map { _print_item($_) } @{ $rota->{items} } );
248
            }
249
            else {
250
                $body .=
251
                  sprintf "\n    No items to be processed for this rota.\n";
252
            }
253
            $body .= sprintf "\n  Log:";      # Rota log details
254
            if ( @{ $rota->{log} } ) {
255
                $body .= join( "", map { _print_item($_) } @{ $rota->{log} } );
256
            }
257
            else {
258
                $body .= sprintf "\n    No items in log for this rota.\n\n";
259
            }
260
        }
261
    }
262
    return [
263
        $header,
264
        {
265
            letter => {
266
                title   => 'Stockrotation Report',
267
                content => $body                     # The body of the report
268
            },
269
            status          => 1,    # We have a meaningful report
270
            no_branch_email => 1,    # We don't expect branch email in report
271
        }
272
    ];
273
}
274
275
=head3 report_email
276
277
  my $email_report = report_email($report);
278
279
Returns an arrayref containing a header string, with basic report information,
280
and any number of 'per_branch' strings, containing a detailed report about the
281
current state of the stockrotation subsystem, from the perspective of those
282
individual branches.
283
284
$REPORT should be the return value of `investigate`, and $BRANCH should be
285
either 0 (to indicate 'all'), or a specific Koha::Library object.
286
287
No data in the database is manipulated by this procedure.
288
289
=cut
290
291
sub report_email {
292
    my ( $data, $branch ) = @_;
293
294
    my $out    = [];
295
    my $header = "";
296
297
    # Summary
298
    my $branched = $data->{branched};
299
    my $flag     = 0;
300
301
    $header .= sprintf "
302
BRANCH-BASED STOCKROTATION REPORT
303
---------------------------------\n";
304
    push @{$out}, $header;
305
306
    if ($branch) {    # Branch limited report
307
        push @{$out}, _report_per_branch( $branched->{ $branch->branchcode } );
308
    }
309
    elsif ( $data->{actionable} ) {    # Full email report
310
        while ( my ( $branchcode_id, $details ) = each %{$branched} ) {
311
            push @{$out}, _report_per_branch($details)
312
              if ( @{ $details->{items} } );
313
        }
314
    }
315
    else {
316
        push @{$out}, {
317
            body => sprintf "
318
No actionable items at any libraries.\n\n",    # The body of the report
319
            no_branch_email => 1,    # We don't expect branch email in report
320
        };
321
    }
322
    return $out;
323
}
324
325
=head3 _report_per_branch
326
327
  my $branch_string = _report_per_branch($branch_details, $branchcode, $branchname);
328
329
return a string containing details about the stockrotation items and their
330
status for the branch identified by $BRANCHCODE.
331
332
This helper procedure is only used from within `report_email`.
333
334
No data in the database is manipulated by this procedure.
335
336
=cut
337
338
sub _report_per_branch {
339
    my ($branch) = @_;
340
341
    my $status = 0;
342
    if ( $branch && @{ $branch->{items} } ) {
343
        $status = 1;
344
    }
345
346
    if (
347
        my $letter = C4::Letters::GetPreparedLetter(
348
            module                 => 'circulation',
349
            letter_code            => "SR_SLIP",
350
            message_transport_type => 'email',
351
            substitute             => $branch
352
        )
353
      )
354
    {
355
        return {
356
            letter        => $letter,
357
            email_address => $branch->{email},
358
            $status
359
        };
360
    }
361
    return;
362
}
363
364
=head3 _print_item
365
366
  my $string = _print_item($item_section);
367
368
Return a string containing an overview about $ITEM_SECTION.
369
370
This helper procedure is only used from within `report_full`.
371
372
No data in the database is manipulated by this procedure.
373
374
=cut
375
376
sub _print_item {
377
    my ($item) = @_;
378
    return sprintf "
379
    Title:           %s
380
    Author:          %s
381
    Callnumber:      %s
382
    Location:        %s
383
    Barcode:         %s
384
    On loan?:        %s
385
    Status:          %s
386
    Current Library: %s [%s]\n\n",
387
      $item->{title}      || "N/A", $item->{author}   || "N/A",
388
      $item->{callnumber} || "N/A", $item->{location} || "N/A",
389
      $item->{barcode} || "N/A", $item->{onloan} ? 'Yes' : 'No',
390
      $item->{reason} || "N/A", $item->{branch}->branchname,
391
      $item->{branch}->branchcode;
392
}
393
394
=head3 emit
395
396
  undef = emit($params);
397
398
$PARAMS should be a hashref of the following format:
399
  admin_email: the address to which a copy of all reports should be sent.
400
  execute: the flag indicating whether we performed db updates
401
  send_all: the flag indicating whether we should send even empty reports
402
  send_email: the flag indicating whether we want to emit to stdout or email
403
  report: the data structure returned from one of the report procedures
404
405
No data in the database is manipulated by this procedure.
406
407
The return value is unspecified: we simply emit a message as a side-effect or
408
die.
409
410
=cut
411
412
sub emit {
413
    my ($params) = @_;
414
415
# REPORT is an arrayref of at least 2 elements:
416
#   - The header for the report, which will be repeated for each part
417
#   - a "part" for each report we want to emit
418
# PARTS are hashrefs:
419
#   - part->{status}: a boolean indicating whether the reported part is empty or not
420
#   - part->{email_address}: the email address to send the report to
421
#   - part->{no_branch_email}: a boolean indicating that we are missing a branch email
422
#   - part->{letter}: a GetPreparedLetter hash as returned by the C4::Letters module
423
    my $report = $params->{report};
424
    my $header = shift @{$report};
425
    my $parts  = $report;
426
427
    my @emails;
428
    foreach my $part ( @{$parts} ) {
429
430
        if ( $part->{status} || $params->{send_all} ) {
431
432
            # We have a report to send, or we want to send even empty
433
            # reports.
434
435
            # Send to branch
436
            my $addressee;
437
            if ( $part->{email_address} ) {
438
                $addressee = $part->{email_address};
439
            }
440
            elsif ( !$part->{no_branch_email} ) {
441
442
#push @emails, "***We tried to send a branch report, but we have no email address for this branch.***\n\n";
443
                $addressee = C4::Context->preference('KohaAdminEmailAddress')
444
                  if ( C4::Context->preference('KohaAdminEmailAddress') );
445
            }
446
447
            if ( $params->{send_email} ) {    # Only email if emails requested
448
                if ( defined($addressee) ) {
449
                    C4::Letters::EnqueueLetter(
450
                        {
451
                            letter                 => $part->{letter},
452
                            to_address             => $addressee,
453
                            message_transport_type => 'email',
454
                        }
455
                      )
456
                      or warn
457
                      "can't enqueue letter $part->{letter} for $addressee";
458
                }
459
460
                # Copy to admin?
461
                if ( $params->{admin_email} ) {
462
                    C4::Letters::EnqueueLetter(
463
                        {
464
                            letter                 => $part->{letter},
465
                            to_address             => $params->{admin_email},
466
                            message_transport_type => 'email',
467
                        }
468
                      )
469
                      or warn
470
"can't enqueue letter $part->{letter} for $params->{admin_email}";
471
                }
472
            }
473
            else {
474
                my $email =
475
                  "-------- Email message --------" . "\n\n" . "To: "
476
                  . defined($addressee)               ? $addressee
477
                  : defined( $params->{admin_email} ) ? $params->{admin_email}
478
                  : '' . "\n"
479
                  . "Subject: "
480
                  . $part->{letter}->{title} . "\n\n"
481
                  . $part->{letter}->{content};
482
                push @emails, $email;
483
            }
484
        }
485
    }
486
487
    # Emit to stdout instead of email?
488
    if ( !$params->{send_email} ) {
489
490
        # The final message is the header + body of this part.
491
        my $msg = $header;
492
        $msg .= "No database updates have been performed.\n\n"
493
          unless ( $params->{execute} );
494
495
        # Append email reports to message
496
        $msg .= join( "\n\n", @emails );
497
        printf $msg;
498
    }
499
}
500
501
#### Main Code
502
503
# Compile Stockrotation Report data
504
my $rotas = Koha::StockRotationRotas->search(undef,{ order_by => { '-asc' => 'title' }});
505
my $data  = $rotas->investigate;
506
507
# Perform db updates if requested
508
execute($data) if ($execute);
509
510
# Emit Reports
511
my $out_report = {};
512
$out_report = report_email( $data, $branch ) if $report eq 'email';
513
$out_report = report_full( $data, $branch ) if $report eq 'full';
514
emit(
515
    {
516
        admin_email => $admin_email,
517
        execute     => $execute,
518
        report      => $out_report,
519
        send_all    => $send_all,
520
        send_email  => $send_email,
521
    }
522
);
523
524
=head1 AUTHOR
525
526
Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
527
528
=cut
(-)a/t/db_dependent/Items.t (+61 lines)
Lines 830-835 subtest 'Test logging for ModItem' => sub { Link Here
830
    $schema->resultset('ActionLog')->search()->delete();
830
    $schema->resultset('ActionLog')->search()->delete();
831
    ModItem({ location => $location }, $bibnum, $itemnumber);
831
    ModItem({ location => $location }, $bibnum, $itemnumber);
832
    is( $schema->resultset('ActionLog')->count(), 1, 'Undefined value defaults to true, triggers logging' );
832
    is( $schema->resultset('ActionLog')->count(), 1, 'Undefined value defaults to true, triggers logging' );
833
};
834
835
subtest 'Check stockrotationitem relationship' => sub {
836
    plan tests => 1;
837
838
    $schema->storage->txn_begin();
839
840
    my $builder = t::lib::TestBuilder->new;
841
    my $item = $builder->build({ source => 'Item' });
842
843
    $builder->build({
844
        source => 'Stockrotationitem',
845
        value  => { itemnumber_id => $item->{itemnumber} }
846
    });
847
848
    my $sritem = Koha::Items->find($item->{itemnumber})->stockrotationitem;
849
    isa_ok( $sritem, 'Koha::StockRotationItem', "Relationship works and correctly creates Koha::Object." );
850
851
    $schema->storage->txn_rollback;
852
};
853
854
subtest 'Check add_to_rota method' => sub {
855
    plan tests => 2;
856
857
    $schema->storage->txn_begin();
858
859
    my $builder = t::lib::TestBuilder->new;
860
    my $item = $builder->build({ source => 'Item' });
861
    my $rota = $builder->build({ source => 'Stockrotationrota' });
862
    my $srrota = Koha::StockRotationRotas->find($rota->{rota_id});
863
864
    $builder->build({
865
        source => 'Stockrotationstage',
866
        value  => { rota_id => $rota->{rota_id} },
867
    });
868
869
    my $sritem = Koha::Items->find($item->{itemnumber});
870
    $sritem->add_to_rota($rota->{rota_id});
871
872
    is(
873
        Koha::StockRotationItems->find($item->{itemnumber})->stage_id,
874
        $srrota->stockrotationstages->next->stage_id,
875
        "Adding to a rota a new sritem item being assigned to its first stage."
876
    );
877
878
    my $newrota = $builder->build({ source => 'Stockrotationrota' });
879
880
    my $srnewrota = Koha::StockRotationRotas->find($newrota->{rota_id});
881
882
    $builder->build({
883
        source => 'Stockrotationstage',
884
        value  => { rota_id => $newrota->{rota_id} },
885
    });
886
887
    $sritem->add_to_rota($newrota->{rota_id});
888
889
    is(
890
        Koha::StockRotationItems->find($item->{itemnumber})->stage_id,
891
        $srnewrota->stockrotationstages->next->stage_id,
892
        "Moving an item results in that sritem being assigned to the new first stage."
893
    );
833
894
834
    $schema->storage->txn_rollback;
895
    $schema->storage->txn_rollback;
835
};
896
};
(-)a/t/db_dependent/Koha/Libraries.t (-1 / +24 lines)
Lines 19-25 Link Here
19
19
20
use Modern::Perl;
20
use Modern::Perl;
21
21
22
use Test::More tests => 4;
22
use Test::More tests => 6;
23
23
24
use Koha::Library;
24
use Koha::Library;
25
use Koha::Libraries;
25
use Koha::Libraries;
Lines 53-58 is( $retrieved_library_1->branchname, $new_library_1->branchname, 'Find a librar Link Here
53
$retrieved_library_1->delete;
53
$retrieved_library_1->delete;
54
is( Koha::Libraries->search->count, $nb_of_libraries + 1, 'Delete should have deleted the library' );
54
is( Koha::Libraries->search->count, $nb_of_libraries + 1, 'Delete should have deleted the library' );
55
55
56
# Stockrotation relationship testing
57
58
my $new_library_sr = $builder->build({ source => 'Branch' });
59
60
$builder->build({
61
    source => 'Stockrotationstage',
62
    value  => { branchcode_id => $new_library_sr->{branchcode} },
63
});
64
$builder->build({
65
    source => 'Stockrotationstage',
66
    value  => { branchcode_id => $new_library_sr->{branchcode} },
67
});
68
$builder->build({
69
    source => 'Stockrotationstage',
70
    value  => { branchcode_id => $new_library_sr->{branchcode} },
71
});
72
73
my $srstages = Koha::Libraries->find($new_library_sr->{branchcode})
74
    ->stockrotationstages;
75
is( $srstages->count, 3, 'Correctly fetched stockrotationstages associated with this branch');
76
77
isa_ok( $srstages->next, 'Koha::StockRotationStage', "Relationship correctly creates Koha::Objects." );
78
56
$schema->storage->txn_rollback;
79
$schema->storage->txn_rollback;
57
80
58
subtest '->get_effective_marcorgcode' => sub {
81
subtest '->get_effective_marcorgcode' => sub {
(-)a/t/db_dependent/StockRotationItems.t (+393 lines)
Line 0 Link Here
1
#!/usr/bin/perl
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 under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
use Modern::Perl;
21
22
use DateTime;
23
use DateTime::Duration;
24
use Koha::Database;
25
use Koha::Item::Transfer;
26
use t::lib::TestBuilder;
27
28
use Test::More tests => 8;
29
30
my $schema = Koha::Database->new->schema;
31
32
use_ok('Koha::StockRotationItems');
33
use_ok('Koha::StockRotationItem');
34
35
my $builder = t::lib::TestBuilder->new;
36
37
subtest 'Basic object tests' => sub {
38
39
    plan tests => 5;
40
41
    $schema->storage->txn_begin;
42
43
    my $itm = $builder->build({ source => 'Item' });
44
    my $stage = $builder->build({ source => 'Stockrotationstage' });
45
46
    my $item = $builder->build({
47
        source => 'Stockrotationitem',
48
        value  => {
49
            itemnumber_id => $itm->{itemnumber},
50
            stage_id      => $stage->{stage_id},
51
        },
52
    });
53
54
    my $sritem = Koha::StockRotationItems->find($item->{itemnumber_id});
55
    isa_ok(
56
        $sritem,
57
        'Koha::StockRotationItem',
58
        "Correctly create and load a stock rotation item."
59
    );
60
61
    # Relationship to rota
62
    isa_ok( $sritem->itemnumber, 'Koha::Item', "Fetched related item." );
63
    is( $sritem->itemnumber->itemnumber, $itm->{itemnumber}, "Related rota OK." );
64
65
    # Relationship to stage
66
    isa_ok( $sritem->stage, 'Koha::StockRotationStage', "Fetched related stage." );
67
    is( $sritem->stage->stage_id, $stage->{stage_id}, "Related stage OK." );
68
69
70
    $schema->storage->txn_rollback;
71
};
72
73
subtest 'Tests for needs_repatriating' => sub {
74
75
    plan tests => 4;
76
77
    $schema->storage->txn_begin;
78
79
    # Setup a pristine stockrotation context.
80
    my $sritem = $builder->build({ source => 'Stockrotationitem' });
81
    my $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id});
82
    $dbitem->itemnumber->homebranch($dbitem->stage->branchcode_id);
83
    $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id);
84
    $dbitem->stage->position(1);
85
86
    my $dbrota = $dbitem->stage->rota;
87
    my $newstage = $builder->build({
88
        source => 'Stockrotationstage',
89
        value => {
90
            rota_id => $dbrota->rota_id,
91
            position => 2,
92
        }
93
    });
94
95
    # - homebranch == holdingbranch [0]
96
    is(
97
        $dbitem->needs_repatriating, 0,
98
        "Homebranch == Holdingbranch."
99
    );
100
101
    my $branch = $builder->build({ source => 'Branch' });
102
    $dbitem->itemnumber->holdingbranch($branch->{branchcode});
103
104
    # - homebranch != holdingbranch [1]
105
    is(
106
        $dbitem->needs_repatriating, 1,
107
        "Homebranch != holdingbranch."
108
    );
109
110
    # Set to incorrect homebranch.
111
    $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id);
112
    $dbitem->itemnumber->homebranch($branch->{branchcode});
113
    # - homebranch != stockrotationstage.branch & not in transit [1]
114
    is(
115
        $dbitem->needs_repatriating, 1,
116
        "Homebranch != StockRotationStage.Branchcode_id & not in transit."
117
    );
118
119
    # Set to in transit (by implication).
120
    $dbitem->stage($newstage->{stage_id});
121
    # - homebranch != stockrotaitonstage.branch & in transit [0]
122
    is(
123
        $dbitem->needs_repatriating, 1,
124
        "homebranch != stockrotaitonstage.branch & in transit."
125
    );
126
127
    $schema->storage->txn_rollback;
128
};
129
130
subtest "Tests for repatriate." => sub {
131
    plan tests => 3;
132
    $schema->storage->txn_begin;
133
    my $sritem = $builder->build({ source => 'Stockrotationitem' });
134
    my $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id});
135
    $dbitem->stage->position(1);
136
    $dbitem->stage->duration(50);
137
    my $branch = $builder->build({ source => 'Branch' });
138
    $dbitem->itemnumber->holdingbranch($branch->{branchcode});
139
140
    # Test a straight up repatriate
141
    ok($dbitem->repatriate, "Repatriation done.");
142
    my $intransfer = $dbitem->itemnumber->get_transfer;
143
    is($intransfer->frombranch, $branch->{branchcode}, "Origin correct.");
144
    is($intransfer->tobranch, $dbitem->stage->branchcode_id, "Target Correct.");
145
146
    $schema->storage->txn_rollback;
147
};
148
149
subtest "Tests for needs_advancing." => sub {
150
    plan tests => 6;
151
    $schema->storage->txn_begin;
152
153
    # Test behaviour of item freshly added to rota.
154
    my $sritem = $builder->build({
155
        source => 'Stockrotationitem',
156
        value  => { 'fresh' => 1, },
157
    });
158
    my $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id});
159
    is($dbitem->needs_advancing, 1, "An item that is fresh will always need advancing.");
160
161
    # Setup a pristine stockrotation context.
162
    $sritem = $builder->build({
163
        source => 'Stockrotationitem',
164
        value => { 'fresh' => 0,}
165
    });
166
    $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id});
167
    $dbitem->itemnumber->homebranch($dbitem->stage->branchcode_id);
168
    $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id);
169
    $dbitem->stage->position(1);
170
    $dbitem->stage->duration(50);
171
172
    my $dbtransfer = Koha::Item::Transfer->new({
173
        'itemnumber'  => $dbitem->itemnumber_id,
174
        'frombranch'  => $dbitem->stage->branchcode_id,
175
        'tobranch'    => $dbitem->stage->branchcode_id,
176
        'datesent'    => DateTime->now,
177
        'datearrived' => undef,
178
        'comments'    => "StockrotationAdvance",
179
    })->store;
180
181
    # Test item will not be advanced if in transit.
182
    is($dbitem->needs_advancing, 0, "Not ready to advance: in transfer.");
183
    # Test item will not be advanced if in transit even if fresh.
184
    $dbitem->fresh(1)->store;
185
    is($dbitem->needs_advancing, 0, "Not ready to advance: in transfer (fresh).");
186
    $dbitem->fresh(0)->store;
187
188
    # Test item will not be advanced if it has not spent enough time.
189
    $dbtransfer->datearrived(DateTime->now)->store;
190
    is($dbitem->needs_advancing, 0, "Not ready to advance: Not spent enough time.");
191
    # Test item will be advanced if it has not spent enough time, but is fresh.
192
    $dbitem->fresh(1)->store;
193
    is($dbitem->needs_advancing, 1, "Advance: Not spent enough time, but fresh.");
194
    $dbitem->fresh(0)->store;
195
196
    # Test item will be advanced if it has spent enough time.
197
    $dbtransfer->datesent(      # Item was sent 100 days ago...
198
        DateTime->now - DateTime::Duration->new( days => 100 )
199
    )->store;
200
    $dbtransfer->datearrived(   # And arrived 75 days ago.
201
        DateTime->now - DateTime::Duration->new( days => 75 )
202
    )->store;
203
    is($dbitem->needs_advancing, 1, "Ready to be advanced.");
204
205
    $schema->storage->txn_rollback;
206
};
207
208
subtest "Tests for advance." => sub {
209
    plan tests => 15;
210
    $schema->storage->txn_begin;
211
212
    my $sritem = $builder->build({
213
        source => 'Stockrotationitem',
214
        value => { 'fresh' => 1 }
215
    });
216
    my $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id});
217
    $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id);
218
    my $dbstage = $dbitem->stage;
219
    $dbstage->position(1)->duration(50)->store; # Configure stage.
220
    # Configure item
221
    $dbitem->itemnumber->holdingbranch($dbstage->branchcode_id)->store;
222
    $dbitem->itemnumber->homebranch($dbstage->branchcode_id)->store;
223
    # Sanity check
224
    is($dbitem->stage->stage_id, $dbstage->stage_id, "Stage sanity check.");
225
226
    # Test if an item is fresh, always move to first stage.
227
    is($dbitem->fresh, 1, "Fresh is correct.");
228
    $dbitem->advance;
229
    is($dbitem->stage->stage_id, $dbstage->stage_id, "Stage is first stage after fresh advance.");
230
    is($dbitem->fresh, 0, "Fresh reset after advance.");
231
232
    # Test cases of single stage
233
    $dbstage->rota->cyclical(1)->store;         # Set Rota to cyclical.
234
    ok($dbitem->advance, "Single stage cyclical advance done.");
235
    ## Refetch dbitem
236
    $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id});
237
    is($dbitem->stage->stage_id, $dbstage->stage_id, "Single stage cyclical stage OK.");
238
239
    # Test with indemand advance
240
    $dbitem->indemand(1)->store;
241
    ok($dbitem->advance, "Indemand item advance done.");
242
    ## Refetch dbitem
243
    $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id});
244
    is($dbitem->indemand, 0, "Indemand OK.");
245
    is($dbitem->stage->stage_id, $dbstage->stage_id, "Indemand item advance stage OK.");
246
247
    # Multi stages
248
    my $srstage = $builder->build({
249
        source => 'Stockrotationstage',
250
        value => { duration => 50 }
251
    });
252
    my $dbstage2 = Koha::StockRotationStages->find($srstage->{stage_id});
253
    $dbstage2->move_to_group($dbitem->stage->rota_id);
254
    $dbstage2->move_last;
255
256
    # Test a straight up advance
257
    ok($dbitem->advance, "Advancement done.");
258
    ## Refetch dbitem
259
    $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id});
260
    ## Test results
261
    is($dbitem->stage->stage_id, $dbstage2->stage_id, "Stage updated.");
262
    my $intransfer = $dbitem->itemnumber->get_transfer;
263
    is($intransfer->frombranch, $dbstage->branchcode_id, "Origin correct.");
264
    is($intransfer->tobranch, $dbstage2->branchcode_id, "Target Correct.");
265
266
    $dbstage->rota->cyclical(0)->store;         # Set Rota to non-cyclical.
267
268
    # Arrive at new branch
269
    $intransfer->datearrived(DateTime->now)->store;
270
    $dbitem->itemnumber->holdingbranch($srstage->{branchcode_id})->store;
271
    $dbitem->itemnumber->homebranch($srstage->{branchcode_id})->store;
272
273
    # Advance again, Remove from rota.
274
    ok($dbitem->advance, "Non-cyclical advance.");
275
    ## Refetch dbitem
276
    $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id});
277
    is($dbitem, undef, "StockRotationItem has been removed.");
278
279
    $schema->storage->txn_rollback;
280
};
281
282
subtest "Tests for investigate (singular)." => sub {
283
    plan tests => 7;
284
    $schema->storage->txn_begin;
285
286
    # Test brand new item's investigation ['initiation']
287
    my $sritem = $builder->build({ source => 'Stockrotationitem', value => { fresh => 1 } });
288
    my $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id});
289
    is($dbitem->investigate->{reason}, 'initiation', "fresh item initiates.");
290
291
    # Test brand new item at stagebranch ['initiation']
292
    $sritem = $builder->build({ source => 'Stockrotationitem', value => { fresh => 1 } });
293
    $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id});
294
    $dbitem->itemnumber->homebranch($dbitem->stage->branchcode_id)->store;
295
    $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id)->store;
296
    is($dbitem->investigate->{reason}, 'initiation', "fresh item at stagebranch initiates.");
297
298
    # Test item not at stagebranch with branchtransfer history ['repatriation']
299
    $sritem = $builder->build({
300
        source => 'Stockrotationitem',
301
        value => { 'fresh'       => 0,}
302
    });
303
    $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id});
304
    my $dbtransfer = Koha::Item::Transfer->new({
305
        'itemnumber'  => $dbitem->itemnumber_id,
306
        'frombranch'  => $dbitem->itemnumber->homebranch,
307
        'tobranch'    => $dbitem->itemnumber->homebranch,
308
        'datesent'    => DateTime->now,
309
        'datearrived' => DateTime->now,
310
        'comments'    => "StockrotationAdvance",
311
    })->store;
312
    is($dbitem->investigate->{reason}, 'repatriation', "older item repatriates.");
313
314
    # Test item at stagebranch with branchtransfer history ['not-ready']
315
    $sritem = $builder->build({
316
        source => 'Stockrotationitem',
317
        value => { 'fresh'       => 0,}
318
    });
319
    $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id});
320
    $dbtransfer = Koha::Item::Transfer->new({
321
        'itemnumber'  => $dbitem->itemnumber_id,
322
        'frombranch'  => $dbitem->itemnumber->homebranch,
323
        'tobranch'    => $dbitem->stage->branchcode_id,
324
        'datesent'    => DateTime->now,
325
        'datearrived' => DateTime->now,
326
        'comments'    => "StockrotationAdvance",
327
    })->store;
328
    $dbitem->itemnumber->homebranch($dbitem->stage->branchcode_id)->store;
329
    $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id)->store;
330
    is($dbitem->investigate->{reason}, 'not-ready', "older item at stagebranch not-ready.");
331
332
    # Test item due for advancement ['advancement']
333
    $sritem = $builder->build({ source => 'Stockrotationitem', value => { fresh => 0 } });
334
    $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id});
335
    $dbitem->indemand(0)->store;
336
    $dbitem->stage->duration(50)->store;
337
    my $sent_duration =  DateTime::Duration->new( days => 55);
338
    my $arrived_duration =  DateTime::Duration->new( days => 52);
339
    $dbtransfer = Koha::Item::Transfer->new({
340
        'itemnumber'  => $dbitem->itemnumber_id,
341
        'frombranch'  => $dbitem->itemnumber->homebranch,
342
        'tobranch'    => $dbitem->stage->branchcode_id,
343
        'datesent'    => DateTime->now - $sent_duration,
344
        'datearrived' => DateTime->now - $arrived_duration,
345
        'comments'    => "StockrotationAdvance",
346
    })->store;
347
    $dbitem->itemnumber->homebranch($dbitem->stage->branchcode_id)->store;
348
    $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id)->store;
349
    is($dbitem->investigate->{reason}, 'advancement',
350
       "Item ready for advancement.");
351
352
    # Test item due for advancement but in-demand ['in-demand']
353
    $sritem = $builder->build({ source => 'Stockrotationitem', value => { fresh => 0 } });
354
    $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id});
355
    $dbitem->indemand(1)->store;
356
    $dbitem->stage->duration(50)->store;
357
    $sent_duration =  DateTime::Duration->new( days => 55);
358
    $arrived_duration =  DateTime::Duration->new( days => 52);
359
    $dbtransfer = Koha::Item::Transfer->new({
360
        'itemnumber'  => $dbitem->itemnumber_id,
361
        'frombranch'  => $dbitem->itemnumber->homebranch,
362
        'tobranch'    => $dbitem->stage->branchcode_id,
363
        'datesent'    => DateTime->now - $sent_duration,
364
        'datearrived' => DateTime->now - $arrived_duration,
365
        'comments'    => "StockrotationAdvance",
366
    })->store;
367
    $dbitem->itemnumber->homebranch($dbitem->stage->branchcode_id)->store;
368
    $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id)->store;
369
    is($dbitem->investigate->{reason}, 'in-demand',
370
       "Item advances, but in-demand.");
371
372
    # Test item ready for advancement, but at wrong library ['repatriation']
373
    $sritem = $builder->build({ source => 'Stockrotationitem', value => { fresh => 0 } });
374
    $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id});
375
    $dbitem->indemand(0)->store;
376
    $dbitem->stage->duration(50)->store;
377
    $sent_duration =  DateTime::Duration->new( days => 55);
378
    $arrived_duration =  DateTime::Duration->new( days => 52);
379
    $dbtransfer = Koha::Item::Transfer->new({
380
        'itemnumber'  => $dbitem->itemnumber_id,
381
        'frombranch'  => $dbitem->itemnumber->homebranch,
382
        'tobranch'    => $dbitem->stage->branchcode_id,
383
        'datesent'    => DateTime->now - $sent_duration,
384
        'datearrived' => DateTime->now - $arrived_duration,
385
        'comments'    => "StockrotationAdvance",
386
    })->store;
387
    is($dbitem->investigate->{reason}, 'repatriation',
388
       "Item advances, but not at stage branch.");
389
390
    $schema->storage->txn_rollback;
391
};
392
393
1;
(-)a/t/db_dependent/StockRotationRotas.t (+175 lines)
Line 0 Link Here
1
#!/usr/bin/perl
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 under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
use Modern::Perl;
21
22
use Koha::Database;
23
use t::lib::TestBuilder;
24
25
use Test::More tests => 5;
26
27
my $schema = Koha::Database->new->schema;
28
29
use_ok('Koha::StockRotationRotas');
30
use_ok('Koha::StockRotationRota');
31
32
subtest 'Basic object tests' => sub {
33
34
    plan tests => 5;
35
36
    $schema->storage->txn_begin;
37
38
    my $builder = t::lib::TestBuilder->new;
39
40
    my $rota = $builder->build({ source => 'Stockrotationrota' });
41
42
    my $srrota = Koha::StockRotationRotas->find($rota->{rota_id});
43
    isa_ok(
44
        $srrota,
45
        'Koha::StockRotationRota',
46
        "Correctly create and load a stock rotation rota."
47
    );
48
49
    $builder->build({
50
        source => 'Stockrotationstage',
51
        value  => { rota_id => $rota->{rota_id} },
52
    });
53
    $builder->build({
54
        source => 'Stockrotationstage',
55
        value  => { rota_id => $rota->{rota_id} },
56
    });
57
    $builder->build({
58
        source => 'Stockrotationstage',
59
        value  => { rota_id => $rota->{rota_id} },
60
    });
61
62
    my $srstages = $srrota->stockrotationstages;
63
    is( $srstages->count, 3, 'Correctly fetched stockrotationstages associated with this rota');
64
65
    isa_ok( $srstages->next, 'Koha::StockRotationStage', "Relationship correctly creates Koha::Objects." );
66
67
    #### Test add_item
68
69
    my $item = $builder->build({ source => 'Item' });
70
71
    $srrota->add_item($item->{itemnumber});
72
73
    is(
74
        Koha::StockRotationItems->find($item->{itemnumber})->stage_id,
75
        $srrota->first_stage->stage_id,
76
        "Adding an item results in a new sritem item being assigned to the first stage."
77
    );
78
79
    my $newrota = $builder->build({ source => 'Stockrotationrota' });
80
81
    my $srnewrota = Koha::StockRotationRotas->find($newrota->{rota_id});
82
83
    $builder->build({
84
        source => 'Stockrotationstage',
85
        value  => { rota_id => $newrota->{rota_id} },
86
    });
87
88
    $srnewrota->add_item($item->{itemnumber});
89
90
    is(
91
        Koha::StockRotationItems->find($item->{itemnumber})->stage_id,
92
        $srnewrota->stockrotationstages->next->stage_id,
93
        "Moving an item results in that sritem being assigned to the new first stage."
94
    );
95
96
    $schema->storage->txn_rollback;
97
};
98
99
subtest '->first_stage test' => sub {
100
    plan tests => 2;
101
102
    $schema->storage->txn_begin;
103
104
    my $builder = t::lib::TestBuilder->new;
105
106
    my $rota = $builder->build({ source => 'Stockrotationrota' });
107
108
    my $stage1 = $builder->build({
109
        source => 'Stockrotationstage',
110
        value  => { rota_id => $rota->{rota_id} },
111
    });
112
    my $stage2 = $builder->build({
113
        source => 'Stockrotationstage',
114
        value  => { rota_id => $rota->{rota_id} },
115
    });
116
    my $stage3 = $builder->build({
117
        source => 'Stockrotationstage',
118
        value  => { rota_id => $rota->{rota_id} },
119
    });
120
121
    my $srrota = Koha::StockRotationRotas->find($rota->{rota_id});
122
    my $srstage2 = Koha::StockRotationStages->find($stage2->{stage_id});
123
    my $firststage = $srstage2->first_sibling || $srstage2;
124
125
    is( $srrota->first_stage->stage_id, $firststage->stage_id, "First stage works" );
126
127
    $srstage2->move_first;
128
129
    is( Koha::StockRotationRotas->find($rota->{rota_id})->first_stage->stage_id, $stage2->{stage_id}, "Stage re-organized" );
130
131
    $schema->storage->txn_rollback;
132
};
133
134
subtest '->items test' => sub {
135
    plan tests => 1;
136
137
    $schema->storage->txn_begin;
138
139
    my $builder = t::lib::TestBuilder->new;
140
141
    my $rota = $builder->build({ source => 'Stockrotationrota' });
142
143
    my $stage1 = $builder->build({
144
        source => 'Stockrotationstage',
145
        value  => { rota_id => $rota->{rota_id} },
146
    });
147
    my $stage2 = $builder->build({
148
        source => 'Stockrotationstage',
149
        value  => { rota_id => $rota->{rota_id} },
150
    });
151
    my $stage3 = $builder->build({
152
        source => 'Stockrotationstage',
153
        value  => { rota_id => $rota->{rota_id} },
154
    });
155
156
    map { $builder->build({
157
        source => 'Stockrotationitem',
158
        value => { stage_id => $_ },
159
    }) } (
160
        $stage1->{stage_id}, $stage1->{stage_id},
161
        $stage2->{stage_id}, $stage2->{stage_id},
162
        $stage3->{stage_id}, $stage3->{stage_id},
163
    );
164
165
    my $srrota = Koha::StockRotationRotas->find($rota->{rota_id});
166
167
    is(
168
        $srrota->stockrotationitems->count,
169
        6, "Correct number of items"
170
    );
171
172
    $schema->storage->txn_rollback;
173
};
174
175
1;
(-)a/t/db_dependent/StockRotationStages.t (+377 lines)
Line 0 Link Here
1
#!/usr/bin/perl
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 under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
use Modern::Perl;
21
22
use Koha::Database;
23
use t::lib::TestBuilder;
24
25
use Test::More tests => 6;
26
27
my $schema = Koha::Database->new->schema;
28
29
use_ok('Koha::StockRotationStages');
30
use_ok('Koha::StockRotationStage');
31
32
my $builder = t::lib::TestBuilder->new;
33
34
subtest 'Basic object tests' => sub {
35
    plan tests => 5;
36
37
    $schema->storage->txn_begin;
38
39
    my $library = $builder->build({ source => 'Branch' });
40
    my $rota = $builder->build({ source => 'Stockrotationrota' });
41
    my $stage = $builder->build({
42
        source => 'Stockrotationstage',
43
        value  => {
44
            branchcode_id => $library->{branchcode},
45
            rota_id       => $rota->{rota_id},
46
        },
47
    });
48
49
    my $srstage = Koha::StockRotationStages->find($stage->{stage_id});
50
    isa_ok(
51
        $srstage,
52
        'Koha::StockRotationStage',
53
        "Correctly create and load a stock rotation stage."
54
    );
55
56
    # Relationship to library
57
    isa_ok( $srstage->branchcode, 'Koha::Library', "Fetched related branch." );
58
    is( $srstage->branchcode->branchcode, $library->{branchcode}, "Related branch OK." );
59
60
    # Relationship to rota
61
    isa_ok( $srstage->rota, 'Koha::StockRotationRota', "Fetched related rota." );
62
    is( $srstage->rota->rota_id, $rota->{rota_id}, "Related rota OK." );
63
64
    $schema->storage->txn_rollback;
65
};
66
67
subtest 'DBIx::Class::Ordered tests' => sub {
68
    plan tests => 33;
69
70
    $schema->storage->txn_begin;
71
72
    my $library = $builder->build({ source => 'Branch' });
73
    my $rota = $builder->build({ source => 'Stockrotationrota' });
74
    my $stagefirst = $builder->build({
75
        source   => 'Stockrotationstage',
76
        value    => { rota_id  => $rota->{rota_id}, position => 1 }
77
    });
78
    my $stageprevious = $builder->build({
79
        source   => 'Stockrotationstage',
80
        value    => { rota_id  => $rota->{rota_id}, position => 2 }
81
    });
82
    my $stage = $builder->build({
83
        source => 'Stockrotationstage',
84
        value  => { rota_id => $rota->{rota_id}, position => 3 },
85
    });
86
    my $stagenext = $builder->build({
87
        source   => 'Stockrotationstage',
88
        value    => { rota_id  => $rota->{rota_id}, position => 4 }
89
    });
90
    my $stagelast = $builder->build({
91
        source   => 'Stockrotationstage',
92
        value    => { rota_id  => $rota->{rota_id}, position => 5 }
93
    });
94
95
    my $srstage = Koha::StockRotationStages->find($stage->{stage_id});
96
97
    is($srstage->siblings->count, 4, "Siblings works.");
98
    is($srstage->previous_siblings->count, 2, "Previous Siblings works.");
99
    is($srstage->next_siblings->count, 2, "Next Siblings works.");
100
101
    my $map = {
102
        first_sibling    => $stagefirst,
103
        previous_sibling => $stageprevious,
104
        next_sibling     => $stagenext,
105
        last_sibling     => $stagelast,
106
    };
107
    # Test plain relations:
108
    while ( my ( $srxsr, $check ) = each %{$map} ) {
109
        my $sr = $srstage->$srxsr;
110
        isa_ok($sr, 'Koha::StockRotationStage', "Fetched using '$srxsr'.");
111
        is($sr->stage_id, $check->{stage_id}, "'$srxsr' data is correct.");
112
    };
113
114
    # Test mutators
115
    ## Move Previous
116
    ok($srstage->move_previous, "Previous.");
117
    is($srstage->previous_sibling->stage_id, $stagefirst->{stage_id}, "Previous, correct previous.");
118
    is($srstage->next_sibling->stage_id, $stageprevious->{stage_id}, "Previous, correct next.");
119
    ## Move Next
120
    ok($srstage->move_next, "Back to middle.");
121
    is($srstage->previous_sibling->stage_id, $stageprevious->{stage_id}, "Middle, correct previous.");
122
    is($srstage->next_sibling->stage_id, $stagenext->{stage_id}, "Middle, correct next.");
123
    ## Move First
124
    ok($srstage->move_first, "First.");
125
    is($srstage->previous_sibling, 0, "First, correct previous.");
126
    is($srstage->next_sibling->stage_id, $stagefirst->{stage_id}, "First, correct next.");
127
    ## Move Last
128
    ok($srstage->move_last, "Last.");
129
    is($srstage->previous_sibling->stage_id, $stagelast->{stage_id}, "Last, correct previous.");
130
    is($srstage->next_sibling, 0, "Last, correct next.");
131
    ## Move To
132
133
    ### Out of range moves.
134
    is(
135
        $srstage->move_to($srstage->siblings->count + 2),
136
        0, "Move above count of stages."
137
    );
138
    is($srstage->move_to(0), 0, "Move to 0th position.");
139
    is($srstage->move_to(-1), 0, "Move to negative position.");
140
141
    ### Move To
142
    ok($srstage->move_to(3), "Move.");
143
    is($srstage->previous_sibling->stage_id, $stageprevious->{stage_id}, "Move, correct previous.");
144
    is($srstage->next_sibling->stage_id, $stagenext->{stage_id}, "Move, correct next.");
145
146
    # Group manipulation
147
    my $newrota = $builder->build({ source => 'Stockrotationrota' });
148
    ok($srstage->move_to_group($newrota->{rota_id}), "Move to Group.");
149
    is(Koha::StockRotationStages->find($srstage->stage_id)->rota_id, $newrota->{rota_id}, "Moved correctly.");
150
151
    # Delete in ordered context
152
    ok($srstage->delete, "Deleted OK.");
153
    is(
154
        Koha::StockRotationStages->find($stageprevious)->next_sibling->stage_id,
155
        $stagenext->{stage_id},
156
        "Delete, correctly re-ordered."
157
    );
158
159
    $schema->storage->txn_rollback;
160
};
161
162
subtest 'Relationship to stockrotationitems' => sub {
163
    plan tests => 2;
164
165
    $schema->storage->txn_begin;
166
    my $stage = $builder->build({ source => 'Stockrotationstage' });
167
168
    $builder->build({
169
        source => 'Stockrotationitem',
170
        value  => { stage_id => $stage->{stage_id} },
171
    });
172
    $builder->build({
173
        source => 'Stockrotationitem',
174
        value  => { stage_id => $stage->{stage_id} },
175
    });
176
    $builder->build({
177
        source => 'Stockrotationitem',
178
        value  => { stage_id => $stage->{stage_id} },
179
    });
180
181
    my $srstage = Koha::StockRotationStages->find($stage->{stage_id});
182
    my $sritems = $srstage->stockrotationitems;
183
    is(
184
        $sritems->count, 3,
185
        'Correctly fetched stockrotationitems associated with this stage'
186
    );
187
188
    isa_ok(
189
        $sritems->next, 'Koha::StockRotationItem',
190
        "Relationship correctly creates Koha::Objects."
191
    );
192
193
    $schema->storage->txn_rollback;
194
};
195
196
197
subtest 'Tests for investigate (singular)' => sub {
198
199
    plan tests => 3;
200
201
    # In this subtest series we will primarily be testing whether items end up
202
    # in the correct 'branched' section of the stage-report.  We don't care
203
    # for item reasons here, as they are tested in StockRotationItems.
204
205
    # We will run tests on first on an empty report (the base-case) and then
206
    # on a populated report.
207
208
    # We will need:
209
    # - Libraries which will hold the Items
210
    # - Rota Which containing the related stages
211
    #   + Stages on which we run investigate
212
    #     * Items on the stages
213
214
    $schema->storage->txn_begin;
215
216
    # Libraries
217
    my $library1 = $builder->build({ source => 'Branch' });
218
    my $library2 = $builder->build({ source => 'Branch' });
219
    my $library3 = $builder->build({ source => 'Branch' });
220
221
    my $stage1lib = $builder->build({ source => 'Branch' });
222
    my $stage2lib = $builder->build({ source => 'Branch' });
223
    my $stage3lib = $builder->build({ source => 'Branch' });
224
    my $stage4lib = $builder->build({ source => 'Branch' });
225
226
    my $libraries = [ $library1, $library2, $library3, $stage1lib, $stage2lib,
227
                      $stage3lib, $stage4lib ];
228
229
    # Rota
230
    my $rota = $builder->build({
231
        source => 'Stockrotationrota',
232
        value  => { cyclical => 0 },
233
    });
234
235
    # Stages
236
    my $stage1 = $builder->build({
237
        source => 'Stockrotationstage',
238
        value  => {
239
            rota_id => $rota->{rota_id},
240
            branchcode_id => $stage1lib->{branchcode},
241
            duration => 10,
242
            position => 1,
243
        },
244
    });
245
    my $stage2 = $builder->build({
246
        source => 'Stockrotationstage',
247
        value  => {
248
            rota_id => $rota->{rota_id},
249
            branchcode_id => $stage2lib->{branchcode},
250
            duration => 20,
251
            position => 2,
252
        },
253
    });
254
    my $stage3 = $builder->build({
255
        source => 'Stockrotationstage',
256
        value  => {
257
            rota_id => $rota->{rota_id},
258
            branchcode_id => $stage3lib->{branchcode},
259
            duration => 10,
260
            position => 3,
261
        },
262
    });
263
    my $stage4 = $builder->build({
264
        source => 'Stockrotationstage',
265
        value  => {
266
            rota_id => $rota->{rota_id},
267
            branchcode_id => $stage4lib->{branchcode},
268
            duration => 20,
269
            position => 4,
270
        },
271
    });
272
273
    # Test on an empty report.
274
    my $spec =  {
275
        $library1->{branchcode} => 1,
276
        $library2->{branchcode} => 1,
277
        $library3->{branchcode} => 1,
278
        $stage1lib->{branchcode} => 2,
279
        $stage2lib->{branchcode} => 1,
280
        $stage3lib->{branchcode} => 3,
281
        $stage4lib->{branchcode} => 4
282
    };
283
    while ( my ( $code, $count ) = each %{$spec} ) {
284
        my $cnt = 0;
285
        while ( $cnt < $count ) {
286
            my $item = $builder->build({
287
                source => 'Stockrotationitem',
288
                value  => {
289
                    stage_id => $stage1->{stage_id},
290
                    indemand => 0,
291
                    fresh    => 1,
292
                }
293
            });
294
            my $dbitem = Koha::StockRotationItems->find($item);
295
            $dbitem->itemnumber->homebranch($code)
296
                ->holdingbranch($code)->store;
297
            $cnt++;
298
        }
299
    }
300
    my $report = Koha::StockRotationStages
301
        ->find($stage1->{stage_id})->investigate;
302
    my $results = [];
303
    foreach my $lib ( @{$libraries} ) {
304
        my $items = $report->{branched}->{$lib->{branchcode}}->{items} || [];
305
        push @{$results},
306
            scalar @{$items};
307
    }
308
309
    # Items assigned to stag1lib -> log, hence $results[4] = 0;
310
    is_deeply( $results, [ 1, 1, 1, 2, 1, 3, 4 ], "Empty report test 1.");
311
312
    # Now we test by adding the next stage's items to the same report.
313
    $spec =  {
314
        $library1->{branchcode} => 3,
315
        $library2->{branchcode} => 2,
316
        $library3->{branchcode} => 1,
317
        $stage1lib->{branchcode} => 4,
318
        $stage2lib->{branchcode} => 2,
319
        $stage3lib->{branchcode} => 0,
320
        $stage4lib->{branchcode} => 3
321
    };
322
    while ( my ( $code, $count ) = each %{$spec} ) {
323
        my $cnt = 0;
324
        while ( $cnt < $count ) {
325
            my $item = $builder->build({
326
                source => 'Stockrotationitem',
327
                value  => {
328
                    stage_id => $stage2->{stage_id},
329
                    indemand => 0,
330
                    fresh => 1,
331
                }
332
            });
333
            my $dbitem = Koha::StockRotationItems->find($item);
334
            $dbitem->itemnumber->homebranch($code)
335
                ->holdingbranch($code)->store;
336
            $cnt++;
337
        }
338
    }
339
340
    $report = Koha::StockRotationStages
341
        ->find($stage2->{stage_id})->investigate($report);
342
    $results = [];
343
    foreach my $lib ( @{$libraries} ) {
344
        my $items = $report->{branched}->{$lib->{branchcode}}->{items} || [];
345
        push @{$results},
346
            scalar @{$items};
347
    }
348
    is_deeply( $results, [ 4, 3, 2, 6, 3, 3, 7 ], "full report test.");
349
350
    # Carry out db updates
351
    foreach my $item (@{$report->{items}}) {
352
        my $reason = $item->{reason};
353
        if ( $reason eq 'repatriation' ) {
354
            $item->{object}->repatriate;
355
        } elsif ( grep { $reason eq $_ }
356
                      qw/in-demand advancement initiation/ ) {
357
            $item->{object}->advance;
358
        }
359
    }
360
361
    $report = Koha::StockRotationStages
362
        ->find($stage1->{stage_id})->investigate;
363
    $results = [];
364
    foreach my $lib ( @{$libraries} ) {
365
        my $items = $report->{branched}->{$lib->{branchcode}}->{items} || [];
366
        push @{$results},
367
            scalar @{$items};
368
    }
369
    # All items have been 'initiated', which means they are either happily in
370
    # transit or happily at the library they are supposed to be.  Either way
371
    # they will register as 'not-ready' in the stock rotation report.
372
    is_deeply( $results, [ 0, 0, 0, 0, 0, 0, 0 ], "All items now in logs.");
373
374
    $schema->storage->txn_rollback;
375
};
376
377
1;
(-)a/t/db_dependent/api/v1/stockrotationstage.t (+172 lines)
Line 0 Link Here
1
#!/usr/bin/env perl
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
6
# terms of the GNU General Public License as published by the Free Software
7
# Foundation; either version 3 of the License, or (at your option) any later
8
# version.
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License along
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18
use Modern::Perl;
19
20
use Test::More tests => 1;
21
use Test::Mojo;
22
use Test::Warn;
23
24
use t::lib::TestBuilder;
25
use t::lib::Mocks;
26
27
use C4::Auth;
28
use Koha::StockRotationStages;
29
30
my $schema  = Koha::Database->new->schema;
31
my $builder = t::lib::TestBuilder->new;
32
33
# FIXME: sessionStorage defaults to mysql, but it seems to break transaction handling
34
# this affects the other REST api tests
35
t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' );
36
37
my $remote_address = '127.0.0.1';
38
my $t              = Test::Mojo->new('Koha::REST::V1');
39
40
subtest 'move() tests' => sub {
41
42
    plan tests => 16;
43
44
    $schema->storage->txn_begin;
45
46
    my ( $unauthorized_borrowernumber, $unauthorized_session_id ) =
47
      create_user_and_session( { authorized => 0 } );
48
    my ( $authorized_borrowernumber, $authorized_session_id ) =
49
      create_user_and_session( { authorized => 1 } );
50
51
    my $library1 = $builder->build({ source => 'Branch' });
52
    my $library2 = $builder->build({ source => 'Branch' });
53
    my $rota = $builder->build({ source => 'Stockrotationrota' });
54
    my $stage1 = $builder->build({
55
        source => 'Stockrotationstage',
56
        value  => {
57
            branchcode_id => $library1->{branchcode},
58
            rota_id       => $rota->{rota_id},
59
        }
60
    });
61
    my $stage2 = $builder->build({
62
        source => 'Stockrotationstage',
63
        value  => {
64
            branchcode_id => $library2->{branchcode},
65
            rota_id       => $rota->{rota_id},
66
        }
67
    });
68
    my $rota_id = $rota->{rota_id};
69
    my $stage1_id = $stage1->{stage_id};
70
71
    # Unauthorized attempt to update
72
    my $tx = $t->ua->build_tx(
73
      PUT => "/api/v1/rotas/$rota_id/stages/$stage1_id/position" =>
74
      json => 2
75
    );
76
    $tx->req->cookies(
77
        { name => 'CGISESSID', value => $unauthorized_session_id } );
78
    $tx->req->env( { REMOTE_ADDR => $remote_address } );
79
    $t->request_ok($tx)->status_is(403);
80
81
    # Invalid attempt to move a stage on a non-existant rota
82
    $tx = $t->ua->build_tx(
83
      PUT => "/api/v1/rotas/99999999/stages/$stage1_id/position" =>
84
      json => 2
85
    );
86
    $tx->req->cookies(
87
        { name => 'CGISESSID', value => $authorized_session_id } );
88
    $tx->req->env( { REMOTE_ADDR => $remote_address } );
89
    $t->request_ok($tx)->status_is(404)
90
      ->json_is( '/error' => "Not found - Invalid rota or stage ID" );
91
92
    # Invalid attempt to move an non-existant stage
93
    $tx = $t->ua->build_tx(
94
      PUT => "/api/v1/rotas/$rota_id/stages/999999999/position" =>
95
      json => 2
96
    );
97
    $tx->req->cookies(
98
        { name => 'CGISESSID', value => $authorized_session_id } );
99
    $tx->req->env( { REMOTE_ADDR => $remote_address } );
100
    $t->request_ok($tx)->status_is(404)
101
      ->json_is( '/error' => "Not found - Invalid rota or stage ID" );
102
103
    # Invalid attempt to move stage to current position
104
    my $curr_position = $stage1->{position};
105
    $tx = $t->ua->build_tx(
106
      PUT => "/api/v1/rotas/$rota_id/stages/$stage1_id/position" =>
107
      json => $curr_position
108
    );
109
    $tx->req->cookies(
110
        { name => 'CGISESSID', value => $authorized_session_id } );
111
    $tx->req->env( { REMOTE_ADDR => $remote_address } );
112
    $t->request_ok($tx)->status_is(400)
113
      ->json_is( '/error' => "Bad request - new position invalid" );
114
115
    # Invalid attempt to move stage to invalid position
116
    $tx = $t->ua->build_tx(
117
      PUT => "/api/v1/rotas/$rota_id/stages/$stage1_id/position" =>
118
      json => 99999999
119
    );
120
    $tx->req->cookies(
121
        { name => 'CGISESSID', value => $authorized_session_id } );
122
    $tx->req->env( { REMOTE_ADDR => $remote_address } );
123
    $t->request_ok($tx)->status_is(400)
124
      ->json_is( '/error' => "Bad request - new position invalid" );
125
126
    # Valid, authorised move
127
    $tx = $t->ua->build_tx(
128
      PUT => "/api/v1/rotas/$rota_id/stages/$stage1_id/position" =>
129
      json => 2
130
    );
131
    $tx->req->cookies(
132
        { name => 'CGISESSID', value => $authorized_session_id } );
133
    $tx->req->env( { REMOTE_ADDR => $remote_address } );
134
    $t->request_ok($tx)->status_is(200);
135
136
    $schema->storage->txn_rollback;
137
};
138
139
sub create_user_and_session {
140
141
    my $args  = shift;
142
    my $flags = ( $args->{authorized} ) ? $args->{authorized} : 0;
143
    my $dbh   = C4::Context->dbh;
144
145
    my $user = $builder->build(
146
        {
147
            source => 'Borrower',
148
            value  => {
149
                flags => $flags
150
            }
151
        }
152
    );
153
154
    # Create a session for the authorized user
155
    my $session = C4::Auth::get_session('');
156
    $session->param( 'number',   $user->{borrowernumber} );
157
    $session->param( 'id',       $user->{userid} );
158
    $session->param( 'ip',       '127.0.0.1' );
159
    $session->param( 'lasttime', time() );
160
    $session->flush;
161
162
    if ( $args->{authorized} ) {
163
        $dbh->do( "
164
            INSERT INTO user_permissions (borrowernumber,module_bit,code)
165
            VALUES (?,3,'parameters_remaining_permissions')", undef,
166
            $user->{borrowernumber} );
167
    }
168
169
    return ( $user->{borrowernumber}, $session->id );
170
}
171
172
1;
(-)a/tools/stockrotation.pl (-1 / +531 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# Copyright 2016 PTFS-Europe Ltd
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
=head1 stockrotation.pl
21
22
 Script to handle stockrotation. Including rotas, their associated stages
23
 and items
24
25
=cut
26
27
use Modern::Perl;
28
use CGI;
29
30
use C4::Auth;
31
use C4::Context;
32
use C4::Output;
33
34
use Koha::Libraries;
35
use Koha::StockRotationRotas;
36
use Koha::StockRotationItems;
37
use Koha::StockRotationStages;
38
use Koha::Item;
39
use Koha::Util::StockRotation qw(:ALL);
40
41
my $input = new CGI;
42
43
unless (C4::Context->preference('StockRotation')) {
44
    # redirect to Intranet home if self-check is not enabled
45
    print $input->redirect("/cgi-bin/koha/mainpage.pl");
46
    exit;
47
}
48
49
my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
50
    {
51
        template_name   => 'tools/stockrotation.tt',
52
        query           => $input,
53
        type            => 'intranet',
54
        flagsrequired   => {
55
            tools => '*',
56
            stockrotation => '*',
57
        },
58
        authnotrequired => 0
59
    }
60
);
61
62
# Grab all passed data
63
# 'our' since Plack changes the scoping
64
# of 'my'
65
our %params = $input->Vars();
66
67
my $op = $params{op};
68
69
if (!defined $op) {
70
71
    # No operation is supplied, we're just displaying the list of rotas
72
    my $rotas = Koha::StockRotationRotas->search(
73
        undef,
74
        {
75
            order_by => { -asc => 'title' }
76
        }
77
    )->as_list;
78
79
    $template->param(
80
        existing_rotas => $rotas,
81
        no_op_set      => 1
82
    );
83
84
} elsif ($op eq 'create_edit_rota') {
85
86
    # Edit an existing rota or define a new one
87
    my $rota_id = $params{rota_id};
88
89
    my $rota = {};
90
91
    if (!defined $rota_id) {
92
93
        # No ID supplied, we're creating a new rota
94
        # Create a shell rota hashref
95
        $rota = {
96
            cyclical => 1
97
        };
98
99
    } else {
100
101
        # ID supplied, we're editing an existing rota
102
        $rota = Koha::StockRotationRotas->find($rota_id);
103
104
    }
105
106
    $template->param(
107
        rota => $rota,
108
        op   => $op
109
    );
110
111
} elsif ($op eq 'toggle_rota') {
112
113
    # Find and update the active status of the rota
114
    my $rota = Koha::StockRotationRotas->find($params{rota_id});
115
116
    my $new_active = ($rota->active == 1) ? 0 : 1;
117
118
    $rota->active($new_active)->store;
119
120
    # Return to rotas page
121
    print $input->redirect('stockrotation.pl');
122
123
} elsif ($op eq 'process_rota') {
124
125
    # Get a hashref of the submitted rota data
126
    my $rota = get_rota_from_form();
127
128
    if (!process_rota($rota)) {
129
130
        # The submitted rota was invalid
131
        $template->param(
132
            error => 'invalid_form',
133
            rota => $rota,
134
            op   => 'create_edit_rota'
135
        );
136
137
    } else {
138
139
        # All was well, return to the rotas list
140
        print $input->redirect('stockrotation.pl');
141
142
    }
143
144
} elsif ($op eq 'manage_stages') {
145
146
    my $rota = Koha::StockRotationRotas->find($params{rota_id});
147
148
    $template->param(
149
        rota            => $rota,
150
        branches        => get_branches(),
151
        existing_stages => get_stages($rota),
152
        rota_id         => $params{rota_id},
153
        op              => $op
154
    );
155
156
} elsif ($op eq 'create_edit_stage') {
157
158
    # Edit an existing stage or define a new one
159
    my $stage_id = $params{stage_id};
160
161
    my $rota_id = $params{rota_id};
162
163
    if (!defined $stage_id) {
164
165
        # No ID supplied, we're creating a new stage
166
        $template->param(
167
            branches => get_branches(),
168
            stage    => {},
169
            rota_id  => $rota_id,
170
            op       => $op
171
        );
172
173
    } else {
174
175
        # ID supplied, we're editing an existing stage
176
        my $stage = Koha::StockRotationStages->find($stage_id);
177
178
        $template->param(
179
            branches => get_branches(),
180
            stage    => $stage,
181
            rota_id  => $stage->rota->rota_id,
182
            op       => $op
183
        );
184
185
    }
186
187
} elsif ($op eq 'confirm_remove_from_rota') {
188
189
    # Get the stage we're deleting
190
    $template->param(
191
        op       => $op,
192
        rota_id  => $params{rota_id},
193
        stage_id => $params{stage_id},
194
        item_id  => $params{item_id}
195
    );
196
197
} elsif ($op eq 'confirm_delete_stage') {
198
199
    # Get the stage we're deleting
200
    my $stage = Koha::StockRotationStages->find($params{stage_id});
201
202
    $template->param(
203
        op    => $op,
204
        stage => $stage
205
    );
206
207
} elsif ($op eq 'delete_stage') {
208
209
    # Get the stage we're deleting
210
    my $stage = Koha::StockRotationStages->find($params{stage_id});
211
212
    # Get the ID of the rota with which this stage is associated
213
    # (so we can return to the "Manage stages" page after deletion)
214
    my $rota_id = $stage->rota->rota_id;
215
216
    $stage->delete;
217
218
    # Return to the stages list
219
    print $input->redirect("?op=manage_stages&rota_id=$rota_id");
220
221
} elsif ($op eq 'process_stage') {
222
223
    # Get a hashref of the submitted stage data
224
    my $stage = get_stage_from_form();
225
226
    # The rota we're managing
227
    my $rota_id = $params{rota_id};
228
229
    if (!process_stage($stage, $rota_id)) {
230
231
        # The submitted stage was invalid
232
        # Get all branches
233
        my $branches = get_branches();
234
235
        $template->param(
236
            error        => 'invalid_form',
237
            all_branches => $branches,
238
            stage        => $stage,
239
            rota_id      => $rota_id,
240
            op           => 'create_edit_stage'
241
        );
242
243
    } else {
244
245
        # All was well, return to the stages list
246
        print $input->redirect("?op=manage_stages&rota_id=$rota_id");
247
248
    }
249
250
} elsif ($op eq 'manage_items') {
251
252
    my $rota = Koha::StockRotationRotas->find($params{rota_id});
253
254
    # Get all items on this rota, for each prefetch their
255
    # stage and biblio objects
256
    my $items = Koha::StockRotationItems->search(
257
        { 'stage.rota_id' => $params{rota_id} },
258
        {
259
            prefetch => {
260
                stage => {
261
                    'stockrotationitems' => {
262
                        'itemnumber' => 'biblionumber'
263
                    }
264
                }
265
            }
266
        }
267
    );
268
269
    $template->param(
270
        rota_id  => $params{rota_id},
271
        error    => $params{error},
272
        items    => $items,
273
        branches => get_branches(),
274
        stages   => get_stages($rota),
275
        rota     => $rota,
276
        op       => $op
277
    );
278
279
} elsif ($op eq 'move_to_next_stage') {
280
281
    move_to_next_stage($params{item_id}, $params{stage_id});
282
283
    # Return to the items list
284
    print $input->redirect("?op=manage_items&rota_id=" . $params{rota_id});
285
286
} elsif ($op eq 'toggle_in_demand') {
287
288
    # Toggle the item's in_demand
289
    toggle_indemand($params{item_id}, $params{stage_id});
290
291
    # Return to the items list
292
    print $input->redirect("?op=manage_items&rota_id=".$params{rota_id});
293
294
} elsif ($op eq 'remove_item_from_stage') {
295
296
    # Remove the item from the stage
297
    remove_from_stage($params{item_id}, $params{stage_id});
298
299
    # Return to the items list
300
    print $input->redirect("?op=manage_items&rota_id=".$params{rota_id});
301
302
} elsif ($op eq 'add_items_to_rota') {
303
304
    # The item's barcode,
305
    # which we may or may not have been passed
306
    my $barcode = $params{barcode};
307
308
    # The rota we're adding the item to
309
    my $rota_id = $params{rota_id};
310
311
    # The uploaded file filehandle,
312
    # which we may or may not have been passed
313
    my $barcode_file = $input->upload("barcodefile");
314
315
    # We need to create an array of one or more barcodes to
316
    # insert
317
    my @barcodes = ();
318
319
    # If the barcode input box was populated, use it
320
    push @barcodes, $barcode if $barcode;
321
322
    # Only parse the uploaded file if necessary
323
    if ($barcode_file) {
324
325
        # Call binmode on the filehandle as we want to set a
326
        # UTF-8 layer on it
327
        binmode($barcode_file, ":encoding(UTF-8)");
328
        # Parse the file into an array of barcodes
329
        while (my $barcode = <$barcode_file>) {
330
            $barcode =~ s/\r/\n/g;
331
            $barcode =~ s/\n+/\n/g;
332
            my @data = split(/\n/, $barcode);
333
            push @barcodes, @data;
334
        }
335
336
    }
337
338
    # A hashref to hold the status of each barcode
339
    my $barcode_status = {
340
        ok        => [],
341
        on_other  => [],
342
        on_this   => [],
343
        not_found => []
344
    };
345
346
    # If we have something to work with, do it
347
    get_barcodes_status($rota_id, \@barcodes, $barcode_status) if (@barcodes);
348
349
    # Now we know the status of each barcode, add those that
350
    # need it
351
    if (scalar @{$barcode_status->{ok}} > 0) {
352
353
        add_items_to_rota($rota_id, $barcode_status->{ok});
354
355
    }
356
    # If we were only passed one barcode and it was successfully
357
    # added, redirect back to ourselves, we don't want to display
358
    # a report, redirect also if we were passed no barcodes
359
    if (
360
        scalar @barcodes == 0 ||
361
        (scalar @barcodes == 1 && scalar @{$barcode_status->{ok}} == 1)
362
    ) {
363
364
        print $input->redirect("?op=manage_items&rota_id=$rota_id");
365
366
    } else {
367
368
        # Report on the outcome
369
        $template->param(
370
            barcode_status => $barcode_status,
371
            rota_id        => $rota_id,
372
            op             => $op
373
        );
374
375
    }
376
377
} elsif ($op eq 'move_items_to_rota') {
378
379
    # The barcodes of the items we're moving
380
    my @move = $input->param('move_item');
381
382
    foreach my $item(@move) {
383
384
        # The item we're moving
385
        my $item = Koha::Items->find($item);
386
387
        # Move it to the new rota
388
        $item->add_to_rota($params{rota_id});
389
390
    }
391
392
    # Return to the items list
393
    print $input->redirect("?op=manage_items&rota_id=".$params{rota_id});
394
395
}
396
397
output_html_with_http_headers $input, $cookie, $template->output;
398
399
sub get_rota_from_form {
400
401
    return {
402
        id          => $params{id},
403
        title       => $params{title},
404
        cyclical    => $params{cyclical},
405
        description => $params{description}
406
    };
407
}
408
409
sub get_stage_from_form {
410
411
    return {
412
        stage_id    => $params{stage_id},
413
        branchcode  => $params{branchcode},
414
        duration    => $params{duration}
415
    };
416
}
417
418
sub process_rota {
419
420
    my $sub_rota = shift;
421
422
    # Fields we require
423
    my @required = ('title','cyclical');
424
425
    # Count of the number of required fields we have
426
    my $valid = 0;
427
428
    # Ensure we have everything we require
429
    foreach my $req(@required) {
430
431
        if (exists $sub_rota->{$req}) {
432
433
            chomp(my $value = $sub_rota->{$req});
434
            if (length $value > 0) {
435
                $valid++;
436
            }
437
438
        }
439
440
    }
441
442
    # If we don't have everything we need
443
    return 0 if $valid != scalar @required;
444
445
    # Passed validation
446
    # Find the rota we're updating
447
    my $rota = Koha::StockRotationRotas->find($sub_rota->{id});
448
449
    if ($rota) {
450
451
        $rota->title(
452
            $sub_rota->{title}
453
        )->cyclical(
454
            $sub_rota->{cyclical}
455
        )->description(
456
            $sub_rota->{description}
457
        )->store;
458
459
    } else {
460
461
        $rota = Koha::StockRotationRota->new({
462
            title       => $sub_rota->{title},
463
            cyclical    => $sub_rota->{cyclical},
464
            active      => 0,
465
            description => $sub_rota->{description}
466
        })->store;
467
468
    }
469
470
    return 1;
471
}
472
473
sub process_stage {
474
475
    my ($sub_stage, $rota_id) = @_;
476
477
    # Fields we require
478
    my @required = ('branchcode','duration');
479
480
    # Count of the number of required fields we have
481
    my $valid = 0;
482
483
    # Ensure we have everything we require
484
    foreach my $req(@required) {
485
486
        if (exists $sub_stage->{$req}) {
487
488
            chomp(my $value = $sub_stage->{$req});
489
            if (length $value > 0) {
490
                $valid++;
491
            }
492
493
        }
494
495
    }
496
497
    # If we don't have everything we need
498
    return 0 if $valid != scalar @required;
499
500
    # Passed validation
501
    # Find the stage we're updating
502
    my $stage = Koha::StockRotationStages->find($sub_stage->{stage_id});
503
504
    if ($stage) {
505
506
        # Updating an existing stage
507
        $stage->branchcode_id(
508
            $sub_stage->{branchcode}
509
        )->duration(
510
            $sub_stage->{duration}
511
        )->store;
512
513
    } else {
514
515
        # Creating a new stage
516
        $stage = Koha::StockRotationStage->new({
517
            branchcode_id  => $sub_stage->{branchcode},
518
            rota_id        => $rota_id,
519
            duration       => $sub_stage->{duration}
520
        })->store;
521
522
    }
523
524
    return 1;
525
}
526
527
=head1 AUTHOR
528
529
Andrew Isherwood <andrew.isherwood@ptfs-europe.com>
530
531
=cut

Return to bug 11897