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

(-)a/Koha/Item.pm (-1 / +48 lines)
Lines 30-35 use Koha::IssuingRules; Link Here
30
use Koha::Item::Transfer;
30
use Koha::Item::Transfer;
31
use Koha::Patrons;
31
use Koha::Patrons;
32
use Koha::Libraries;
32
use Koha::Libraries;
33
use Koha::StockRotationItem;
34
use Koha::StockRotationRotas;
33
35
34
use base qw(Koha::Object);
36
use base qw(Koha::Object);
35
37
Lines 235-241 sub current_holds { Link Here
235
    return Koha::Holds->_new_from_dbic($hold_rs);
237
    return Koha::Holds->_new_from_dbic($hold_rs);
236
}
238
}
237
239
238
=head3 type
240
=head3 stockrotationitem
241
242
  my $sritem = Koha::Item->stockrotationitem;
243
244
Returns the stock rotation item associated with the current item.
245
246
=cut
247
248
sub stockrotationitem {
249
    my ( $self ) = @_;
250
    my $rs = $self->_result->stockrotationitem;
251
    return 0 if !$rs;
252
    return Koha::StockRotationItem->_new_from_dbic( $rs );
253
}
254
255
=head3 add_to_rota
256
257
  my $item = $item->add_to_rota($rota_id);
258
259
Add this item to the rota identified by $ROTA_ID, which means associating it
260
with the first stage of that rota.  Should this item already be associated
261
with a rota, then we will move it to the new rota.
262
263
=cut
264
265
sub add_to_rota {
266
    my ( $self, $rota_id ) = @_;
267
    Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber);
268
    return $self;
269
}
270
271
=head3 biblio
272
273
  my $biblio = $item->biblio;
274
275
Returns the biblio associated with the current item.
276
277
=cut
278
279
sub biblio {
280
    my ( $self ) = @_;
281
    my $rs = $self->_result->biblio;
282
    return Koha::Biblio->_new_from_dbic( $rs );
283
}
284
285
=head3 _type
239
286
240
=cut
287
=cut
241
288
(-)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 (+48 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
sub move {
26
    my $c = shift->openapi->valid_input or return;
27
    my $input = $c->validation->output;
28
29
    my $rota  = Koha::StockRotationRotas->find( $input->{rota_id} );
30
    my $stage = Koha::StockRotationStages->find( $input->{stage_id} );
31
32
    if ( $stage && $rota ) {
33
        my $result = $stage->move_to( $input->{position} );
34
        return $c->render( openapi => {}, status => 200 ) if $result;
35
        return $c->render(
36
            openapi => { error => "Bad request - new position invalid" },
37
            status  => 400
38
        );
39
    }
40
    else {
41
        return $c->render(
42
            openapi => { error => "Not found - Invalid rota or stage ID" },
43
            status  => 404
44
        );
45
    }
46
}
47
48
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 (+124 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
sub object_class {
55
    return 'Koha::StockRotationItem';
56
}
57
58
=head3 investigate
59
60
  my $report = $items->investigate;
61
62
Return a stockrotation report about this set of stockrotationitems.
63
64
In this part of the overall investigation process we split individual item
65
reports into appropriate action segments of our items report and increment
66
some counters.
67
68
The report generated here will be used on the stage level to slot our item
69
reports into appropriate sections of the branched report.
70
71
For details of intent and context of this procedure, please see
72
Koha::StockRotationRota->investigate.
73
74
=cut
75
76
sub investigate {
77
    my ( $self ) = @_;
78
79
    my $items_report = {
80
        items => [],
81
        log => [],
82
        initiable_items => [],
83
        repatriable_items => [],
84
        advanceable_items => [],
85
        indemand_items => [],
86
        actionable => 0,
87
        stationary => 0,
88
    };
89
    while ( my $item = $self->next ) {
90
        my $report = $item->investigate;
91
        if ( $report->{reason} eq 'initiation' ) {
92
            $items_report->{initiable}++;
93
            $items_report->{actionable}++;
94
            push @{$items_report->{items}}, $report;
95
            push @{$items_report->{initiable_items}}, $report;
96
        } elsif ( $report->{reason} eq 'repatriation' ) {
97
            $items_report->{repatriable}++;
98
            $items_report->{actionable}++;
99
            push @{$items_report->{items}}, $report;
100
            push @{$items_report->{repatriable_items}}, $report;
101
        } elsif ( $report->{reason} eq 'advancement' ) {
102
            $items_report->{actionable}++;
103
            push @{$items_report->{items}}, $report;
104
            push @{$items_report->{advanceable_items}}, $report;
105
        } elsif ( $report->{reason} eq 'in-demand' ) {
106
            $items_report->{actionable}++;
107
            push @{$items_report->{items}}, $report;
108
            push @{$items_report->{indemand_items}}, $report;
109
        } else {
110
            $items_report->{stationary}++;
111
            push @{$items_report->{log}}, $report;
112
        }
113
    }
114
115
    return $items_report;
116
}
117
118
1;
119
120
=head1 AUTHOR
121
122
Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
123
124
=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 (+101 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
sub object_class {
92
    return 'Koha::StockRotationRota';
93
}
94
95
1;
96
97
=head1 AUTHOR
98
99
Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
100
101
=cut
(-)a/Koha/StockRotationStage.pm (+413 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->branchemail,
337
            phone => $stagebranch->branchphone,
338
            items => [],
339
            log => [],
340
        };
341
    }
342
343
    push @{$report->{branched}->{$stagebranchcode}->{items}},
344
        @{$items_report->{advanceable_items}};
345
    push @{$report->{branched}->{$stagebranchcode}->{log}},
346
        @{$items_report->{log}};
347
    push @{$report->{branched}->{$stagebranchcode}->{items}},
348
        @{$items_report->{indemand_items}};
349
350
    ### 'Initiable' & 'Repatriable'
351
    foreach my $ireport (@{$items_report->{initiable_items}}) {
352
        my $branch = $ireport->{branch};
353
        my $branchcode = $branch->branchcode;
354
        if ( !$report->{branched}->{$branchcode} ) {
355
            $report->{branched}->{$branchcode} = {
356
                code  => $branchcode,
357
                name  => $branch->branchname,
358
                email => $branch->branchemail,
359
                phone => $branch->branchphone,
360
                items => [],
361
                log => [],
362
            };
363
        }
364
        push @{$report->{branched}->{$branchcode}->{items}}, $ireport;
365
    }
366
367
    foreach my $ireport (@{$items_report->{repatriable_items}}) {
368
        my $branch = $ireport->{branch};
369
        my $branchcode = $branch->branchcode;
370
        if ( !$report->{branched}->{$branchcode} ) {
371
            $report->{branched}->{$branchcode} = {
372
                code  => $branchcode,
373
                name  => $branch->branchname,
374
                email => $branch->branchemail,
375
                phone => $branch->branchphone,
376
                items => [],
377
                log => [],
378
            };
379
        }
380
        push @{$report->{branched}->{$branchcode}->{items}}, $ireport;
381
    }
382
383
    ## Per rota indexes
384
    ### Per rota indexes are item reports pushed into the index for the
385
    ### current rota.  We don't know where that index is yet as we don't know
386
    ### about the current rota.  To resolve this we assign our items and log
387
    ### to tmp indexes.  They will be merged into the proper rota index at the
388
    ### rota level.
389
    push @{$report->{tmp_items}}, @{$items_report->{items}};
390
    push @{$report->{tmp_log}}, @{$items_report->{log}};
391
392
    ## Collection of items
393
    ### Finally we just add our collection of items to the full item index.
394
    push @{$report->{items}}, @{$items_report->{items}};
395
396
    ## Assemble counters
397
    $report->{actionable} += $items_report->{actionable};
398
    $report->{indemand} += scalar @{$items_report->{indemand_items}};
399
    $report->{advanceable} += scalar @{$items_report->{advanceable_items}};
400
    $report->{initiable} += scalar @{$items_report->{initiable_items}};
401
    $report->{repatriable} += scalar @{$items_report->{repatriable_items}};
402
    $report->{stationary} += scalar @{$items_report->{log}};
403
404
    return $report;
405
}
406
407
1;
408
409
=head1 AUTHOR
410
411
Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
412
413
=cut
(-)a/Koha/StockRotationStages.pm (+86 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
sub object_class {
77
    return 'Koha::StockRotationStage';
78
}
79
80
1;
81
82
=head1 AUTHOR
83
84
Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
85
86
=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 25-29 Link Here
25
  },
25
  },
26
  "/illrequests": {
26
  "/illrequests": {
27
    "$ref": "paths/illrequests.json#/~1illrequests"
27
    "$ref": "paths/illrequests.json#/~1illrequests"
28
  },
29
  "/rotas/{rota_id}/stages/{stage_id}/position": {
30
    "$ref": "paths/rotas.json#/~1rotas~1{rota_id}~1stages~1{stage_id}~1position"
28
  }
31
  }
29
}
32
}
(-)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 (+175 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   => { catalogue => 1 }
61
    }
62
);
63
64
if (!defined $op) {
65
66
    # List all items along with their associated rotas
67
    my $biblio = Koha::Biblios->find($biblionumber);
68
69
    my $items = $biblio->items;
70
71
    # Get only rotas with stages
72
    my $rotas = Koha::StockRotationRotas->search(
73
        {
74
            'stockrotationstages.stage_id' => { '!=', undef }
75
        },
76
        {
77
            join     => 'stockrotationstages',
78
            collapse => 1
79
        }
80
    );
81
82
    # Construct a model to pass to the view
83
    my @item_data = ();
84
85
    while (my $item = $items->next) {
86
87
        my $item_hashref = {
88
            bib_item   => $item
89
        };
90
91
        my $stockrotationitem = $item->stockrotationitem;
92
93
        # If this item is on a rota
94
        if ($stockrotationitem != 0) {
95
96
            # This item's rota
97
            my $rota = $stockrotationitem->stage->rota;
98
99
            # This rota's stages
100
            my $stages = get_stages($rota);
101
102
            $item_hashref->{rota} = $rota;
103
104
            $item_hashref->{stockrotationitem} = $stockrotationitem;
105
106
            $item_hashref->{stages} = $stages;
107
108
        }
109
110
        push @item_data, $item_hashref;
111
112
    }
113
114
    $template->param(
115
        no_op_set         => 1,
116
        rotas             => $rotas,
117
        items             => \@item_data,
118
        branches          => get_branches(),
119
        biblio            => $biblio,
120
        biblionumber      => $biblio->biblionumber,
121
        stockrotationview => 1,
122
        C4::Search::enabled_staff_search_views
123
    );
124
125
} elsif ($op eq "toggle_in_demand") {
126
127
    # Toggle in demand
128
    toggle_indemand($params{item_id}, $params{stage_id});
129
130
    # Return to items list
131
    print $input->redirect("?biblionumber=$biblionumber");
132
133
} elsif ($op eq "remove_item_from_stage") {
134
135
    # Remove from the stage
136
    remove_from_stage($params{item_id}, $params{stage_id});
137
138
    # Return to items list
139
    print $input->redirect("?biblionumber=$biblionumber");
140
141
} elsif ($op eq "move_to_next_stage") {
142
143
    move_to_next_stage($params{item_id}, $params{stage_id});
144
145
    # Return to items list
146
    print $input->redirect("?biblionumber=" . $params{biblionumber});
147
148
} elsif ($op eq "add_item_to_rota") {
149
150
    my $item = Koha::Items->find($params{item_id});
151
152
    $item->add_to_rota($params{rota_id});
153
154
    print $input->redirect("?biblionumber=" . $params{biblionumber});
155
156
} elsif ($op eq "confirm_remove_from_rota") {
157
158
    $template->param(
159
        op                => $params{op},
160
        stage_id          => $params{stage_id},
161
        item_id           => $params{item_id},
162
        biblionumber      => $params{biblionumber},
163
        stockrotationview => 1,
164
        C4::Search::enabled_staff_search_views
165
    );
166
167
}
168
169
output_html_with_http_headers $input, $cookie, $template->output;
170
171
=head1 AUTHOR
172
173
Andrew Isherwood <andrew.isherwood@ptfs-europe.com>
174
175
=cut
(-)a/koha-tmpl/intranet-tmpl/prog/css/staff-global.css (+128 lines)
Lines 3218-3220 span.name { Link Here
3218
    font-weight: bold;
3218
    font-weight: bold;
3219
    font-style: italic;
3219
    font-style: italic;
3220
}
3220
}
3221
3222
#stockrotation h3 {
3223
    margin: 30px 0 10px 0;
3224
}
3225
3226
#stockrotation .dialog h3 {
3227
    margin: 10px 0;
3228
}
3229
3230
#stockrotation .dialog {
3231
    margin-bottom: 20px;
3232
}
3233
3234
#stockrotation .highlight_stage,
3235
#catalog_stockrotation .highlight_stage {
3236
    font-weight: bold;
3237
}
3238
3239
#stockrotation #rota_form textarea {
3240
    width: 300px;
3241
    height: 100px;
3242
}
3243
3244
#stockrotation #rota_form #name {
3245
    width: 300px;
3246
}
3247
3248
#stockrotation #rota_form fieldset,
3249
#stockrotation #stage_form fieldset,
3250
#stockrotation #add_rota_item_form fieldset {
3251
    width: auto;
3252
}
3253
3254
#stockrotation .dialog.alert ul {
3255
    margin: 20px 0;
3256
}
3257
3258
#stockrotation .dialog.alert li {
3259
    list-style-type: none;
3260
}
3261
3262
#catalog_stockrotation .item_select_rota {
3263
    vertical-align: middle;
3264
}
3265
3266
#catalog_stockrotation h1 {
3267
    margin-bottom: 20px;
3268
}
3269
3270
#stockrotation td.actions,
3271
#catalog_stockrotation td.actions {
3272
    vertical-align: middle;
3273
}
3274
3275
#stockrotation .stage,
3276
#catalog_stockrotation .stage {
3277
    display: inline-block;
3278
    padding: 5px 7px;
3279
    margin: 3px 0 3px 0;
3280
    border-radius: 5px;
3281
    background-color: rgba(0,0,0,0.1);
3282
}
3283
3284
#stage_list_headings {
3285
    font-weight: bold;
3286
}
3287
3288
#stage_list_headings span {
3289
    padding: 3px;
3290
}
3291
3292
#manage_stages ul {
3293
    padding-left: 0;
3294
}
3295
3296
#manage_stages li {
3297
    list-style: none;
3298
    margin-bottom: 5px;
3299
}
3300
3301
#manage_stages li span {
3302
    padding: 6px 3px;
3303
}
3304
3305
#manage_stages .stagename {
3306
    width: 15em;
3307
    display: inline-block;
3308
}
3309
3310
#manage_stages .stageduration {
3311
    width: 10em;
3312
    display: inline-block;
3313
}
3314
3315
#manage_stages .stageactions {
3316
    display: inline-block;
3317
}
3318
3319
#manage_stages li:nth-child(odd) {
3320
    background-color : #F3F3F3;
3321
}
3322
3323
#manage_stages .drag_handle {
3324
    margin-right: 6px;
3325
    cursor: move;
3326
}
3327
3328
#manage_stages .drag_placeholder {
3329
    height: 2em;
3330
    border: 1px dotted #aaa;
3331
}
3332
3333
#manage_stages h3 {
3334
    display: inline-block;
3335
}
3336
3337
#manage_stages #ajax_status {
3338
    display: inline-block;
3339
    border: 1px solid #bcbcbc;
3340
    border-radius: 5px;
3341
    padding: 5px;
3342
    margin-left: 10px;
3343
    background: #f3f3f3;
3344
}
3345
3346
#manage_stages #manage_stages_help {
3347
    margin: 20px 0;
3348
}
(-)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_can_add_items_rotas && 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/stockrotation-toolbar.inc (+13 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="addstage" class="btn btn-default btn-sm" href="/cgi-bin/koha/tools/stockrotation.pl?op=create_edit_stage&amp;rota_id=[% rota_id %]"><i class="fa fa-plus"></i> New stage</a>
8
    [% END %]
9
    [% IF op == 'manage_items' %]
10
        <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>
11
        <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>
12
    [% END %]
13
</div>
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc (+22 lines)
Lines 1-3 Link Here
1
[% USE Koha %]
2
3
<script type="text/javascript">//<![CDATA[
4
    $(document).ready(function() {
5
        var path = location.pathname.substring(1);
6
        if (path.indexOf("labels") >= 0 && path.indexOf("spine") < 0 ) {
7
          $('#navmenulist a[href$="/cgi-bin/koha/labels/label-home.pl"]').css('font-weight','bold');
8
        } else if (path.indexOf("patroncards") >= 0 ) {
9
          $('#navmenulist a[href$="/cgi-bin/koha/patroncards/home.pl"]').css('font-weight','bold');
10
        } else if (path.indexOf("patron_lists") >= 0 ) {
11
          $('#navmenulist a[href$="/cgi-bin/koha/patron_lists/lists.pl"]').css('font-weight','bold');
12
        } else if ((path+location.search).indexOf("batchMod.pl?del=1") >= 0 ) {
13
          $('#navmenulist a[href$="/cgi-bin/koha/tools/batchMod.pl?del=1"]').css('font-weight','bold');
14
        } else {
15
          $('#navmenulist a[href$="/' + path + '"]').css('font-weight','bold');
16
        }
17
    });
18
//]]>
19
</script>
1
<div id="navmenu">
20
<div id="navmenu">
2
<div id="navmenulist">
21
<div id="navmenulist">
3
<ul>
22
<ul>
Lines 38-43 Link Here
38
    [% IF ( CAN_user_tools_batch_upload_patron_images ) %]
57
    [% IF ( CAN_user_tools_batch_upload_patron_images ) %]
39
	<li><a href="/cgi-bin/koha/tools/picture-upload.pl">Upload patron images</a></li>
58
	<li><a href="/cgi-bin/koha/tools/picture-upload.pl">Upload patron images</a></li>
40
    [% END %]
59
    [% END %]
60
    [% IF ( CAN_user_stockrotation_can_edit_rotas && Koha.Preference('StockRotation') ) %]
61
    <li><a href="/cgi-bin/koha/tools/stockrotation.pl">Stock rotation</a></li>
62
    [% END %]
41
</ul>
63
</ul>
42
<h5>Catalog</h5>
64
<h5>Catalog</h5>
43
<ul>
65
<ul>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref (-1 / +1 lines)
Lines 901-907 Circulation: Link Here
901
                  yes: Enable
901
                  yes: Enable
902
                  no: Disable
902
                  no: Disable
903
            - "housebound module"
903
            - "housebound module"
904
    Stockrotation module:
904
    Stock Rotation module:
905
        -
905
        -
906
            - pref: StockRotation
906
            - pref: StockRotation
907
              choices:
907
              choices:
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/stockrotation.tt (+157 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
<!--[if lt IE 9]>
8
<script type="text/javascript" src="[% interface %]/lib/shims/json2.min.js"></script>
9
<![endif]-->
10
<script type="text/javascript" src="[% interface %]/js/browser.js"></script>
11
<script type="text/javascript">
12
//<![CDATA[
13
    var browser = KOHA.browser('[% searchid %]', parseInt('[% biblionumber %]', 10));
14
    browser.show();
15
//]]>
16
</script>
17
</head>
18
<body id="catalog_stockrotation" class="catalog">
19
[% USE KohaDates %]
20
[% INCLUDE 'header.inc' %]
21
[% INCLUDE 'cat-search.inc' %]
22
23
<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>
24
25
<div id="doc3" class="yui-t2">
26
27
   <div id="bd">
28
    <div id="yui-main">
29
    <div class="yui-b">
30
31
<div id="catalogue_detail_biblio">
32
33
    [% IF no_op_set %]
34
        <h1 class="title">Stock rotation details for [% biblio.title | html %]</h1>
35
        [% IF rotas.count > 0 && items.size > 0 %]
36
37
            <table class="items_table dataTable no-footer" role="grid">
38
                <thead>
39
                    <tr>
40
                        <th>Barcode</th>
41
                        <th>Shelfmark</th>
42
                        <th>Rota</th>
43
                        <th>Rota status</th>
44
                        <th>In transit</th>
45
                        <th>Stages &amp; duration in days<br>(current stage highlighted)</th>
46
                        <th>&nbsp;</th>
47
                    </tr>
48
                </thead>
49
                <tbody>
50
                    [% FOREACH item IN items %]
51
                        <tr>
52
                            <td>[% item.bib_item.barcode %]</td>
53
                            <td>[% item.bib_item.itemcallnumber %]</td>
54
                            <td>
55
                                [% item.rota.title %]
56
                            </td>
57
                            <td>
58
                                [% IF item.rota %]
59
                                    [% IF !item.rota.active %]
60
                                        <span class="highlighted-row">
61
                                    [% END %]
62
                                        [% item.rota.active ? 'Active' : 'Inactive' %]
63
64
                                    [% IF !item.rota.active %]
65
                                        </span>
66
                                    [% END %]
67
                                [% END %]
68
                            </td>
69
                            <td>
70
                                [% item.bib_item.get_transfer ? 'Yes' : 'No' %]
71
                            </td>
72
                            <td>
73
                                [% FOREACH this_stage IN item.stages %]
74
                                    [% IF this_stage.stage_id == item.stockrotationitem.stage.stage_id %]
75
                                        <span class="stage highlight_stage">
76
                                    [% ELSE %]
77
                                        <span class="stage">
78
                                    [% END %]
79
                                    [% Branches.GetName(this_stage.branchcode_id) %] ([% this_stage.duration %])
80
                                    </span>
81
                                    &raquo;
82
                                [% END %]
83
                                [% IF item.stages.size > 0 %]
84
                                    <span class="stage">[% item.rota.cyclical ? 'START' : 'END' %]</span>
85
                                [% END %]
86
                            </td>
87
                            <td class="actions">
88
                                [% IF item.stockrotationitem %]
89
                                    [% in_transit = item.bib_item.get_transfer %]
90
                                    [% IF !in_transit && item.stages.size > 1 %]
91
                                        <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 %]">
92
                                    [% ELSE %]
93
                                        <a class="btn btn-default btn-xs" disabled>
94
                                    [% END %]
95
                                        <i class="fa fa-arrow-right"></i>
96
                                        Move to next stage
97
                                    </a>
98
                                    [% IF !in_transit %]
99
                                        <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 %]">
100
                                    [% ELSE %]
101
                                        <a class="btn btn-default btn-xs" disabled>
102
                                    [% END %]
103
                                        <i class="fa fa-fire"></i>
104
                                        [% item.stockrotationitem.indemand ? 'Remove &quot;In demand&quot;' : 'Add &quot;In demand&quot;' %]
105
                                    </a>
106
                                    [% IF !in_transit %]
107
                                        <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 %]">
108
                                    [% ELSE %]
109
                                        <a class="btn btn-default btn-xs" disabled>
110
                                    [% END %]
111
                                        <i class="fa fa-trash"></i>
112
                                        Remove from rota
113
                                    </a>
114
                                [% ELSE %]
115
                                    <form class="rota_select_form" method="post" enctype="multipart/form-data">
116
                                        <select class="item_select_rota" name="rota_id">
117
                                            [% FOREACH rota IN rotas %]
118
                                                <option value="[% rota.rota_id %]">[% rota.title %]</option>
119
                                            [% END %]
120
                                        </select>
121
                                        <button class="btn btn-default btn-xs" type="submit"><i class="fa fa-plus"></i> Add to rota</button>
122
                                        <input type="hidden" name="op" value="add_item_to_rota"></input>
123
                                        <input type="hidden" name="item_id" value="[% item.bib_item.id %]"></input>
124
                                        <input type="hidden" name="biblionumber" value="[% biblionumber %]"></input>
125
                                    </form>
126
                                [% END %]
127
                            </td>
128
                        </tr>
129
                    [% END %]
130
                </tbody>
131
            </table>
132
        [% END %]
133
        [% IF !items || items.size == 0 %]
134
            <h1>No physical items for this record</h1>
135
        [% END %]
136
        [% IF !rotas || rotas.count == 0 %]
137
            <h1>There are no rotas with stages assigned</h1>
138
        [% END %]
139
    [% ELSIF op == 'confirm_remove_from_rota' %]
140
        <div class="dialog alert">
141
            <h3>Are you sure you want to remove this item from it's rota?</h3>
142
            <p>
143
                <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>
144
                <a class="btn btn-default btn-xs deny" href="?biblionumber=[% biblionumber %]"><i class="fa fa-fw fa-remove"></i>No</a>
145
            </p>
146
        </div>
147
    [% END %]
148
149
</div>
150
151
</div>
152
</div>
153
<div class="yui-b">
154
[% INCLUDE 'biblio-view-menu.inc' %]
155
</div>
156
</div>
157
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stockrotation.tt (+482 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
<link rel="stylesheet" type="text/css" href="[% interface %]/[% theme %]/css/datatables.css" />
8
[% INCLUDE 'datatables.inc' %]
9
<script type="text/javascript" src="[% interface %]/[% theme %]/js/pages/stockrotation.js"></script>
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
                <h1>Stock rotation</h1>
64
65
                [% IF no_op_set %]
66
67
                    [% INCLUDE 'stockrotation-toolbar.inc' %]
68
69
                    [% IF existing_rotas.size > 0 %]
70
                        <h3>Manage existing rotas</h3>
71
                        <table class="rotas_table" role="grid">
72
                            <thead>
73
                                <th>Name</th>
74
                                <th>Cyclical</th>
75
                                <th>Active</th>
76
                                <th>Description</th>
77
                                <th>Number of items</th>
78
                                <th>&nbsp;</th>
79
                            </thead>
80
                            <tbody>
81
                                [% FOREACH rota IN existing_rotas %]
82
                                    <tr>
83
                                        <td>[% rota.title %]</td>
84
                                        <td>[% rota.cyclical ? 'Yes' : 'No'%]</td>
85
                                        <td>[% rota.active ? 'Yes' : 'No'%]</td>
86
                                        <td>[% rota.description %]</td>
87
                                        <td>[% rota.stockrotationitems.count %]</td>
88
                                        <td class="actions">
89
                                            <a class="btn btn-default btn-xs" href="?op=create_edit_rota&amp;rota_id=[% rota.rota_id %]">
90
                                                <i class="fa fa-pencil"></i>
91
                                                Edit
92
                                            </a>
93
                                            <div class="btn-group" role="group">
94
                                                <button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
95
                                                    Manage
96
                                                    <i class="fa fa-caret-down"></i>
97
                                                </button>
98
                                                <ul class="dropdown-menu">
99
                                                    <li><a href="?op=manage_stages&amp;rota_id=[% rota.rota_id %]">Stages</a></li>
100
                                                    [% IF CAN_user_stockrotation_can_add_items_rotas && rota.stockrotationstages.count > 0 %]
101
                                                    <li><a href="?op=manage_items&amp;rota_id=[% rota.rota_id %]">Items</a></li>
102
                                                    [% END %]
103
                                                </ul>
104
                                            </div>
105
                                            <a class="btn btn-default btn-xs" href="?op=toggle_rota&amp;rota_id=[% rota.rota_id %]">
106
                                                <i class="fa fa-power-off"></i>
107
                                                [% IF !rota.active %]
108
                                                    Activate
109
                                                [% ELSE %]
110
                                                    Deactivate
111
                                                [% END %]
112
                                            </a>
113
                                        </td>
114
                                    </tr>
115
                                [% END %]
116
                            </tbody>
117
                        </table>
118
                    [% END %]
119
120
                [% ELSIF (op == 'create_edit_rota') %]
121
122
                    [% IF rota.rota_id %]
123
                        <h3>Edit "[% rota.title %]"</h3>
124
                    [% ELSE %]
125
                        <h3>Create new rota</h3>
126
                    [% END %]
127
128
                    [% IF error == 'invalid_form' %]
129
                    <div class="dialog alert">
130
                        <h3>There was a problem with your form submission</h3>
131
                    </div>
132
                    [% END %]
133
134
                    <form id="rota_form" method="post" enctype="multipart/form-data" class="validated">
135
                        <fieldset class="rows">
136
                            <ol>
137
                                <li>
138
                                    <label class="required" for="title">Name:</label>
139
                                    <input type="text" id="title" name="title" value="[% rota.title %]" required="required" placeholder="Rota name">
140
                                    <span class="required">Required</span>
141
                                </li>
142
                                <li>
143
                                    <label for="cyclical">Cyclical:</label>
144
                                    <select name="cyclical" id="cyclical">
145
                                        [% IF rota.cyclical %]
146
                                            <option value="1" selected>Yes</option>
147
                                            <option value="0">No</option>
148
                                        [% ELSE %]
149
                                            <option value="1">Yes</option>
150
                                            <option value="0" selected>No</option>
151
                                        [% END %]
152
                                    </select>
153
                                </li>
154
                                <li>
155
                                    <label for="active">Active:</label>
156
                                    <select name="active" id="active">
157
                                        [% IF rota.active %]
158
                                            <option value="1" selected>Yes</option>
159
                                            <option value="0">No</option>
160
                                        [% ELSE %]
161
                                            <option value="1">Yes</option>
162
                                            <option value="0" selected>No</option>
163
                                        [% END %]
164
                                    </select>
165
                                </li>
166
                                <li>
167
                                    <label for="description">Description:</label>
168
                                    <textarea id="description" name="description" placeholder="Rota description">[% rota.description %]</textarea>
169
                                </li>
170
                            </ol>
171
                        </fieldset>
172
                        <fieldset class="action">
173
                            <input type="submit" value="Save">
174
                            <a href="/cgi-bin/koha/tools/stockrotation.pl" class="cancel">Cancel</a>
175
                        </fieldset>
176
                        [% IF rota.rota_id %]
177
                            <input type="hidden" name="id" value="[% rota.rota_id %]">
178
                        [% END %]
179
                        <input type="hidden" name="op" value="process_rota">
180
                    </form>
181
182
                [% ELSIF (op == 'manage_stages') %]
183
184
                    [% INCLUDE 'stockrotation-toolbar.inc' %]
185
186
                    [% IF existing_stages.size > 0 %]
187
                        <div id="manage_stages">
188
                            <h3>Manage stages</h3>
189
                            <div id="ajax_status"
190
                                data-saving-msg="Saving changes..."
191
                                data-success-msg=""
192
                                data-failed-msg="Error: ">
193
                                <span id="ajax_saving_msg"></span>
194
                                <i id="ajax_saving_icon" class="fa fa-spinner fa-spin"></i>
195
                                <i id="ajax_success_icon" class="fa fa-check"></i>
196
                                <i id="ajax_failed_icon" class="fa fa-times"></i>
197
                                <span id="ajax_success_msg"></span>
198
                                <span id="ajax_failed_msg"></span>
199
                            </div>
200
                            <div id="manage_stages_help">
201
                                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
202
                            </div>
203
                            <div id="stage_list_headings">
204
                                <span class="stagename">Branch</span>
205
                                <span class="stageduration">Duration (days)</span>
206
                            </div>
207
                            <ul id="sortable_stages" data-rota-id="[% rota.rota_id %]">
208
                                [% FOREACH stage IN existing_stages %]
209
                                    <li id="stage_[% stage.stage_id %]">
210
                                        <span data-toggle="tooltip" title="Drag and drop to move this stage to another position" data-placement="right" class="stagename">
211
                                            [% IF existing_stages.size > 1 %]
212
                                                <i class="drag_handle fa fa-lg fa-bars"></i>
213
                                            [% END %]
214
                                            [% Branches.GetName(stage.branchcode_id) %]
215
                                        </span>
216
                                        <span class="stageduration">[% stage.duration %]</span>
217
                                        <span class="stageactions">
218
                                            <a class="btn btn-default btn-xs" href="?op=create_edit_stage&amp;stage_id=[% stage.stage_id %]">
219
                                                <i class="fa fa-pencil"></i> Edit
220
                                            </a>
221
                                            <a class="btn btn-default btn-xs" href="?op=confirm_delete_stage&amp;stage_id=[% stage.stage_id %]">
222
                                                <i class="fa fa-trash"></i> Delete
223
                                            </a>
224
                                        </span>
225
                                    </li>
226
                                [% END %]
227
                            </ul>
228
                        </div>
229
                    [% END %]
230
231
                    <p><a href="stockrotation.pl">Return to rotas</a></p>
232
233
                [% ELSIF (op == 'create_edit_stage') %]
234
235
                    [% IF stage.id %]
236
                        <h3>Edit "[% Branches.GetName(stage.branchcode_id) %]"</h3>
237
                    [% ELSE %]
238
                        <h3>Create new stage</h3>
239
                    [% END %]
240
241
                    [% IF error == 'invalid_form' %]
242
                    <div class="dialog alert">
243
                        <h3>There was a problem with your form submission</h3>
244
                    </div>
245
                    [% END %]
246
247
                    <form id="stage_form" method="post" enctype="multipart/form-data" class="validated">
248
                        <fieldset class="rows">
249
                            <ol>
250
                                <li>
251
                                    <label class="required" for="branch">Branch:</label>
252
                                    <select name="branchcode" id="branch">
253
                                        [% FOREACH branch IN branches %]
254
                                            [% IF branch.branchcode == stage.branchcode_id %]
255
                                                <option value="[% branch.branchcode %]" selected>[% Branches.GetName(branch.branchcode) %]</option>
256
                                            [% ELSE %]
257
                                                <option value="[% branch.branchcode %]">[% Branches.GetName(branch.branchcode) %]</option>
258
                                            [% END %]
259
                                        [% END %]
260
                                    </select>
261
                                    <span class="required">Required</span>
262
                                </li>
263
                                <li>
264
                                    <label class="required" for="duration">Duration:</label>
265
                                    <input type="text" id="duration" name="duration" value="[% stage.duration %]" required="required" placeholder="Duration (days)">
266
                                    <span class="required">Required</span>
267
                                </li>
268
                            </ol>
269
                        </fieldset>
270
                        <fieldset class="action">
271
                            <input type="submit" value="Save">
272
                            <a href="/cgi-bin/koha/tools/stockrotation.pl?op=manage_stages&amp;rota_id=[% rota_id %]" class="cancel">Cancel</a>
273
                        </fieldset>
274
                        <input type="hidden" name="stage_id" value="[% stage.id %]">
275
                        <input type="hidden" name="rota_id" value="[% rota_id %]">
276
                        <input type="hidden" name="op" value="process_stage">
277
                    </form>
278
                [% ELSIF (op == 'confirm_remove_from_rota') %]
279
280
                    <div class="dialog alert">
281
                        <h3>Are you sure you wish to remove this item from it's rota</h3>
282
                        <p>
283
                            <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>
284
                            <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>
285
                        </p>
286
                    </div>
287
                [% ELSIF (op == 'confirm_delete_stage') %]
288
289
                    <div class="dialog alert">
290
                        <h3>Are you sure you want to delete this stage?</h3>
291
                        [% IF stage.stockrotationitems.count > 0 %]
292
                            <p>This stage contains the following item(s):</p>
293
                            <ul>
294
                                [% FOREACH item IN stage.stockrotationitems %]
295
                                    <li>[% item.itemnumber.biblio.title %] (Barcode: [% item.itemnumber.barcode %])</li>
296
                                [% END %]
297
                            </ul>
298
                        [% END %]
299
                        <p>
300
                            <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>
301
                            <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>
302
                        </p>
303
                    </div>
304
                [% ELSIF (op == 'manage_items') %]
305
306
                    [% INCLUDE 'stockrotation-toolbar.inc' %]
307
308
                    [% IF error %]
309
                        <div class="dialog alert">
310
                            [% IF error == "item_not_found" %]
311
                                <h3>The item was not found</h3>
312
                            [% ELSIF error == "already_on_rota" %]
313
                                <h3>This item is already on this rota</h3>
314
                            [% END %]
315
                        </div>
316
                    [% END %]
317
318
                    <div>
319
                        <form id="add_rota_item_form" method="post" enctype="multipart/form-data" class="validated">
320
                            <fieldset class="rows">
321
                                <legend>Add item to &quot;[% rota.title %]&quot;</legend>
322
                                <ol>
323
                                    <li>
324
                                        <label for="barcode">Barcode:</label>
325
                                        <input type="text" id="barcode" name="barcode" placeholder="Item barcode" autofocus>
326
                                    </li>
327
                                </ol>
328
                            </fieldset>
329
                            <fieldset class="rows">
330
                                <legend>Use a barcode file</legend>
331
                                <ol>
332
                                    <li>
333
                                        <label for="barcodefile">Barcode file:</label>
334
                                        <input type="file" id="barcodefile" name="barcodefile">
335
                                    </li>
336
                                </ol>
337
                            </fieldset>
338
                            <fieldset class="action">
339
                                <input type="submit" value="Save">
340
                            </fieldset>
341
                            <input type="hidden" name="rota_id" value="[% rota.id %]">
342
                            <input type="hidden" name="op" value="add_items_to_rota">
343
                        </form>
344
                    </div>
345
346
                    [% IF items.count > 0 %]
347
                        <h3>Manage items assigned to &quot;[% rota.title %]&quot;</h3>
348
                        <table id="sr_manage_items" class="items_table" role="grid">
349
                            <thead>
350
                                <th>Barcode</th>
351
                                <th>Title</th>
352
                                <th>Author</th>
353
                                <th>Shelfmark</th>
354
                                <th class="NoSearch">In transit</th>
355
                                <th class="NoSort">Stages &amp; duration in days<br>(current stage highlighted)</th>
356
                                <th class="NoSort">&nbsp;</th>
357
                            </thead>
358
                            <tbody>
359
                                [% FOREACH item IN items %]
360
                                    <tr>
361
                                        <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>
362
                                        <td><a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% item.itemnumber.biblio.id %]">[% item.itemnumber.biblio.title %]</a></td>
363
                                        <td>[% item.itemnumber.biblio.author %]</td>
364
                                        <td>[% item.itemnumber.itemcallnumber %]</td>
365
                                        <td>[% item.itemnumber.get_transfer ? 'Yes' : 'No' %]</td>
366
                                        <td>
367
                                            [% FOREACH this_stage IN stages %]
368
                                                [% IF this_stage.stage_id == item.stage.stage_id %]
369
                                                    <span class="stage highlight_stage">
370
                                                [% ELSE %]
371
                                                    <span class="stage">
372
                                                [% END %]
373
                                                [% Branches.GetName(this_stage.branchcode_id) %] ([% this_stage.duration %])
374
                                                </span>
375
                                                &raquo;
376
                                            [% END %]
377
                                            [% IF stages.size > 0 %]
378
                                                <span class="stage">[% rota.cyclical ? 'START' : 'END' %]</span>
379
                                            [% END %]
380
                                        </td>
381
                                        <td class="actions">
382
                                            [% in_transit = item.itemnumber.get_transfer %]
383
                                            [% IF !in_transit && stages.size > 1 %]
384
                                                <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 %]">
385
                                            [% ELSE %]
386
                                                <a class="btn btn-default btn-xs" disabled>
387
                                            [% END %]
388
                                                <i class="fa fa-arrow-right"></i>
389
                                                Move to next stage
390
                                            </a>
391
                                            [% IF !in_transit %]
392
                                                <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 %]">
393
                                            [% ELSE %]
394
                                                <a class="btn btn-default btn-xs" disabled>
395
                                            [% END %]
396
                                                <i class="fa fa-fire"></i>
397
                                                [% item.indemand ? 'Remove &quot;In demand&quot;' : 'Add &quot;In demand&quot;' %]
398
                                            </a>
399
                                            [% IF !in_transit %]
400
                                                <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 %]">
401
                                            [% ELSE %]
402
                                                <a class="btn btn-default btn-xs" disabled>
403
                                            [% END %]
404
                                                <i class="fa fa-trash"></i>
405
                                                Remove from rota
406
                                            </a>
407
                                        </td>
408
                                    </tr>
409
                                [% END %]
410
                            </tbody>
411
                        </table>
412
                    [% END %]
413
414
                    <p><a href="stockrotation.pl">Return to rotas</a></p>
415
416
                [% ELSIF op == 'add_items_to_rota' %]
417
418
419
                    <div class="dialog message">
420
                        <h3>Add items to rota report</h3>
421
                    </div>
422
                    <div>
423
                        [% IF barcode_status.ok.size > 0 %]
424
                            <h4>Items added to rota:</h4>
425
                            <ul>
426
                                [% FOREACH item_ok IN barcode_status.ok %]
427
                                    <li>[% item_ok.biblio.title %]</li>
428
                                [% END %]
429
                            </ul>
430
                        [% END %]
431
                        [% IF barcode_status.on_this.size > 0 %]
432
                            <h4>Items already on this rota:</h4>
433
                            <ul>
434
                                [% FOREACH item_on_this IN barcode_status.on_this %]
435
                                    <li>[% item_on_this.biblio.title %]</li>
436
                                [% END %]
437
                            </ul>
438
                        [% END %]
439
                        [% IF barcode_status.not_found.size > 0 %]
440
                            <h4>Barcodes not found:</h4>
441
                            <ul>
442
                                [% FOREACH barcode_not_found IN barcode_status.not_found %]
443
                                    <li>[% barcode_not_found %]</li>
444
                                [% END %]
445
                            </ul>
446
                        [% END %]
447
                        [% IF barcode_status.on_other.size > 0 %]
448
                            <h4>Items found on other rotas:</h4>
449
                            <ul>
450
                                [% FOREACH item_on_other IN barcode_status.on_other %]
451
                                    <li>[% item_on_other.biblio.title %]</li>
452
                                [% END %]
453
                            </ul>
454
                        [% END %]
455
                    </div>
456
                    [% IF barcode_status.on_other.size > 0 %]
457
                        <form id="add_rota_item_form" method="post" enctype="multipart/form-data">
458
                            <fieldset>
459
                                <legend>Select items to move to this rota:</legend>
460
                                [% FOREACH item_on_other IN barcode_status.on_other %]
461
                                    <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>
462
                                [% END %]
463
464
                            </fieldset>
465
                            <fieldset class="action">
466
                                <input type="submit" value="Save">
467
                            </fieldset>
468
                            <input type="hidden" name="rota_id" value="[% rota_id %]">
469
                            <input type="hidden" name="op" value="move_items_to_rota">
470
                        </form>
471
                    [% END %]
472
                    <p><a href="?op=manage_items&amp;rota_id=[% rota_id %]">Return to rota</a></p>
473
474
                [% END %]
475
            </div>
476
        </div>
477
        <div class="yui-b">
478
            [% INCLUDE 'tools-menu.inc' %]
479
        </div>
480
    </div>
481
</div>
482
[% 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 68-73 Link Here
68
    [% IF ( CAN_user_tools_batch_upload_patron_images ) %]
70
    [% IF ( CAN_user_tools_batch_upload_patron_images ) %]
69
    <dt><a href="/cgi-bin/koha/tools/picture-upload.pl">Upload patron images</a></dt>
71
    <dt><a href="/cgi-bin/koha/tools/picture-upload.pl">Upload patron images</a></dt>
70
	<dd>Upload patron images in a batch or one at a time</dd>
72
	<dd>Upload patron images in a batch or one at a time</dd>
73
    [% END %]
74
75
    [% IF ( CAN_user_stockrotation_can_edit_rotas && Koha.Preference('StockRotation') ) %]
76
    <dt><a href="/cgi-bin/koha/tools/stockrotation.pl">Stock rotation</a></dt>
77
    <dd>Manage Stock rotation rotas, rota stages and rota items</dd>
71
    [% END %]
78
    [% END %]
72
	</dl>
79
	</dl>
73
</div>
80
</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 (+489 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 Mail::Sendmail;
113
use C4::Context;
114
use Koha::Email;
115
use Koha::StockRotationRotas;
116
117
my $admin_email = '';
118
my $branch = 0;
119
my $execute = 0;
120
my $report = 'full';
121
my $send_all = 0;
122
my $send_email = 0;
123
124
my $ok = GetOptions(
125
    'admin-email|a=s' => \$admin_email,
126
    'branchcode|b=s' => sub {
127
        my ($opt_name, $opt_value) = @_;
128
        my $branches = Koha::Libraries->search(
129
            {}, { order_by => { -asc => 'branchname'} }
130
        );
131
        my $brnch = $branches->find($opt_value);
132
        if ( $brnch ) {
133
            $branch = $brnch;
134
            return $brnch;
135
        } else {
136
            printf("Option $opt_name should be one of (name -> code):\n");
137
            while ( my $candidate = $branches->next ) {
138
                printf(
139
                    "  %-40s  ->  %s\n", $candidate->branchname,
140
                    $candidate->branchcode
141
                );
142
            }
143
            exit 1
144
        }
145
    },
146
    'execute|x' => \$execute,
147
    'report|r=s' => sub {
148
        my ($opt_name, $opt_value) = @_;
149
        if ( $opt_value eq 'full' || $opt_value eq 'email' ) {
150
            $report = $opt_value;
151
        } else {
152
            printf("Option $opt_name should be either 'email' or 'full'.\n");
153
            exit 1;
154
        }
155
    },
156
    'send-all|S' => \$send_all,
157
    'send-email|s' => \$send_email,
158
    'help|h|?' => sub { HelpMessage }
159
);
160
exit 1 unless ( $ok );
161
162
$send_email++ if ( $send_all ); # if we send all, then we must want emails.
163
164
=head2 Helpers
165
166
=head3 execute
167
168
  undef = execute($report);
169
170
Perform the database updates, within a transaction, that are reported as
171
needing to be performed by $REPORT.
172
173
$REPORT should be the return value of an invocation of `investigate`.
174
175
This procedure WILL mess with your database.
176
177
=cut
178
179
sub execute {
180
    my ( $data ) = @_;
181
182
    # Begin transaction
183
    my $schema = Koha::Database->new->schema;
184
    $schema->storage->txn_begin;
185
186
    # Carry out db updates
187
    foreach my $item (@{$data->{items}}) {
188
        my $reason = $item->{reason};
189
        if ( $reason eq 'repatriation' ) {
190
            $item->{object}->repatriate;
191
        } elsif ( grep { $reason eq $_ }
192
                      qw/in-demand advancement initiation/ ) {
193
            $item->{object}->advance;
194
        }
195
    }
196
197
    # End transaction
198
    $schema->storage->txn_commit;
199
}
200
201
=head3 report_full
202
203
  my $full_report = report_full($report);
204
205
Return an arrayref containing a string containing a detailed report about the
206
current state of the stockrotation subsystem.
207
208
$REPORT should be the return value of `investigate`.
209
210
No data in the database is manipulated by this procedure.
211
212
=cut
213
214
sub report_full {
215
    my ( $data ) = @_;
216
217
    my $header = "";
218
    my $body = "";
219
    # Summary
220
    $header .= sprintf "
221
STOCKROTATION REPORT
222
--------------------
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 .= join("", map { _print_item($_) } @{$rota->{items}});
247
            } else {
248
                $body .= sprintf
249
                    "\n    No items to be processed for this rota.\n";
250
            }
251
            $body .= sprintf "\n  Log:";  # Rota log details
252
            if (@{$rota->{log}}) {
253
                $body .= join("", map { _print_item($_) } @{$rota->{log}});
254
            } else {
255
                $body .= sprintf "\n    No items in log for this rota.\n\n";
256
            }
257
        }
258
    }
259
    return [ $header, {
260
        body            => $body, # The body of the report
261
        status          => 1,     # We have a meaningful report
262
        no_branch_email => 1,     # We don't expect branch email in report
263
    } ];
264
}
265
266
=head3 report_email
267
268
  my $email_report = report_email($report);
269
270
Returns an arrayref containing a header string, with basic report information,
271
and any number of 'per_branch' strings, containing a detailed report about the
272
current state of the stockrotation subsystem, from the perspective of those
273
individual branches.
274
275
$REPORT should be the return value of `investigate`, and $BRANCH should be
276
either 0 (to indicate 'all'), or a specific Koha::Library object.
277
278
No data in the database is manipulated by this procedure.
279
280
=cut
281
282
sub report_email {
283
    my ( $data, $branch ) = @_;
284
285
    my $out = [];
286
    my $header = "";
287
    # Summary
288
    my $branched = $data->{branched};
289
    my $flag = 0;
290
291
    $header .= sprintf "
292
BRANCH-BASED STOCKROTATION REPORT
293
---------------------------------\n";
294
    push @{$out}, $header;
295
296
    if ( $branch ) {                      # Branch limited report
297
        push @{$out}, _report_per_branch(
298
            $branched->{$branch->branchcode}, $branch->branchcode,
299
            $branch->branchname
300
        );
301
    } elsif ( $data->{actionable} ) {     # Full email report
302
        while ( my ($branchcode_id, $details) = each %{$branched} ) {
303
            push @{$out}, _report_per_branch(
304
                $details, $details->{code}, $details->{name}
305
            ) if ( @{$details->{items}} );
306
        }
307
    } else {
308
        push @{$out}, {
309
            body            => sprintf "
310
No actionable items at any libraries.\n\n", # The body of the report
311
            no_branch_email => 1, # We don't expect branch email in report
312
        };
313
    }
314
    return $out;
315
}
316
317
=head3 _report_per_branch
318
319
  my $branch_string = _report_per_branch($branch_details, $branchcode, $branchname);
320
321
return a string containing details about the stockrotation items and their
322
status for the branch identified by $BRANCHCODE.
323
324
This helper procedure is only used from within `report_email`.
325
326
No data in the database is manipulated by this procedure.
327
328
=cut
329
330
sub _report_per_branch {
331
    my ( $per_branch, $branchcode, $branchname ) = @_;
332
333
    my $out = "";
334
    my $status = 0;
335
336
    $out .= sprintf "\nStockrotation report for '%s' [%s]:\n",
337
        $branchname || "N/A", $branchcode || "N/A";
338
    if ( $per_branch && @{$per_branch->{items}} ) {
339
        $out .= sprintf "
340
  Email: %s
341
  Phone: %s
342
  Items:",
343
        $per_branch->{email} || "N/A", $per_branch->{phone} || "N/A";
344
        $status++;
345
    } else {
346
        $out .= sprintf "No items to be processed for this branch.\n\n";
347
    }
348
    $out .= join("", map {
349
        _print_item($_) unless $_->{reason} eq 'in-demand'
350
    } @{$per_branch->{items}});
351
    return {
352
        body          => $out,                 # The body of the report
353
        email_address => $per_branch->{email}, # The branch email address
354
        status        => $status,              # We may have empty report...
355
    };
356
}
357
358
=head3 _print_item
359
360
  my $string = _print_item($item_section);
361
362
Return a string containing an overview about $ITEM_SECTION.
363
364
This helper procedure is only used from within `report_email` and
365
`report_full`.
366
367
No data in the database is manipulated by this procedure.
368
369
=cut
370
371
sub _print_item {
372
    my ( $item ) = @_;
373
    return sprintf "
374
    Title:           %s
375
    Author:          %s
376
    Callnumber:      %s
377
    Location:        %s
378
    Barcode:         %s
379
    On loan?:        %s
380
    Status:          %s
381
    Current Library: %s [%s]\n\n",
382
          $item->{title} || "N/A", $item->{author} || "N/A",
383
          $item->{callnumber} || "N/A", $item->{location} || "N/A",
384
          $item->{barcode} || "N/A", $item->{onloan} ?  'Yes' : 'No',
385
          $item->{reason} || "N/A", $item->{branch}->branchname,
386
          $item->{branch}->branchcode;
387
}
388
389
=head3 emit
390
391
  undef = emit($params);
392
393
$PARAMS should be a hashref of the following format:
394
  admin_email: the address to which a copy of all reports should be sent.
395
  execute: the flag indicating whether we performed db updates
396
  send_all: the flag indicating whether we should send even empty reports
397
  send_email: the flag indicating whether we want to emit to stdout or email
398
  report: the data structure returned from one of the report procedures
399
400
No data in the database is manipulated by this procedure.
401
402
The return value is unspecified: we simply emit a message as a side-effect or
403
die.
404
405
=cut
406
407
sub emit {
408
    my ( $params ) = @_;
409
410
    # REPORT is an arrayref of at least 2 elements:
411
    #   - The header for the report, which will be repeated for each part
412
    #   - a "part" for each report we want to emit
413
    # PARTS are hashrefs:
414
    #   - part->{body}: the body of the report
415
    #   - part->{email_address}: the email address to send the report to
416
    my $report = $params->{report};
417
    my $header = shift @{$report};
418
    my $parts = $report;
419
420
    foreach my $part (@{$parts}) {
421
        # The final message is the header + body of this part.
422
        my $msg = $header . $part->{body};
423
        $msg .= "No database updates have been performed.\n\n"
424
            unless ( $params->{execute} );
425
426
        if ( $params->{send_email} ) { # Only email if emails requested
427
            if ( $part->{status} || $params->{send_all} ) {
428
                # We have a report to send, or we want to send even empty
429
                # reports.
430
                my $message = Koha::Email->new;
431
432
                # Collate 'to' addresses
433
                my @to = ();
434
                if ( $part->{email_address} ) {
435
                    push @to, $part->{email_address};
436
                } elsif ( !$part->{no_branch_email} ) {
437
                    $msg = "***We tried to send a branch report, but we have no email address for this branch.***\n\n" . $msg;
438
                    push @to, C4::Context->preference('KohaAdminEmailAddress')
439
                        if ( C4::Context->preference('KohaAdminEmailAddress') );
440
                }
441
                push @to, $params->{admin_email} if $params->{admin_email};
442
443
                # Create email data.
444
                my %mail = $message->create_message_headers(
445
                    {
446
                        to          => join("; ", @to),
447
                        subject     => "Stockrotation Email Report",
448
                        message     => Encode::encode("utf8", $msg),
449
                        contenttype => 'text/plain; charset="utf8"',
450
                    }
451
                );
452
453
                # Send or die.
454
                die($Mail::Sendmail::error)
455
                    unless Mail::Sendmail::sendmail(%mail);
456
            }
457
458
        } else {                       #  Else emit to stdout.
459
            printf $msg;
460
        }
461
    }
462
}
463
464
#### Main Code
465
466
# Compile Stockrotation Report data
467
my $rotas = Koha::StockRotationRotas->search;
468
my $data = $rotas->investigate;
469
470
# Perform db updates if requested
471
execute($data) if ( $execute );
472
473
# Emit Reports
474
my $out_report = {};
475
$out_report = report_email($data, $branch) if $report eq 'email';
476
$out_report = report_full($data, $branch) if $report eq 'full';
477
emit({
478
    admin_email => $admin_email,
479
    execute     => $execute,
480
    report      => $out_report,
481
    send_all    => $send_all,
482
    send_email  => $send_email,
483
});
484
485
=head1 AUTHOR
486
487
Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
488
489
=cut
(-)a/t/db_dependent/Items.t (+61 lines)
Lines 833-838 subtest 'Test logging for ModItem' => sub { Link Here
833
    $schema->resultset('ActionLog')->search()->delete();
833
    $schema->resultset('ActionLog')->search()->delete();
834
    ModItem({ location => $location }, $bibnum, $itemnumber);
834
    ModItem({ location => $location }, $bibnum, $itemnumber);
835
    is( $schema->resultset('ActionLog')->count(), 1, 'Undefined value defaults to true, triggers logging' );
835
    is( $schema->resultset('ActionLog')->count(), 1, 'Undefined value defaults to true, triggers logging' );
836
};
837
838
subtest 'Check stockrotationitem relationship' => sub {
839
    plan tests => 1;
840
841
    $schema->storage->txn_begin();
842
843
    my $builder = t::lib::TestBuilder->new;
844
    my $item = $builder->build({ source => 'Item' });
845
846
    $builder->build({
847
        source => 'Stockrotationitem',
848
        value  => { itemnumber_id => $item->{itemnumber} }
849
    });
850
851
    my $sritem = Koha::Items->find($item->{itemnumber})->stockrotationitem;
852
    isa_ok( $sritem, 'Koha::StockRotationItem', "Relationship works and correctly creates Koha::Object." );
853
854
    $schema->storage->txn_rollback;
855
};
856
857
subtest 'Check add_to_rota method' => sub {
858
    plan tests => 2;
859
860
    $schema->storage->txn_begin();
861
862
    my $builder = t::lib::TestBuilder->new;
863
    my $item = $builder->build({ source => 'Item' });
864
    my $rota = $builder->build({ source => 'Stockrotationrota' });
865
    my $srrota = Koha::StockRotationRotas->find($rota->{rota_id});
866
867
    $builder->build({
868
        source => 'Stockrotationstage',
869
        value  => { rota_id => $rota->{rota_id} },
870
    });
871
872
    my $sritem = Koha::Items->find($item->{itemnumber});
873
    $sritem->add_to_rota($rota->{rota_id});
874
875
    is(
876
        Koha::StockRotationItems->find($item->{itemnumber})->stage_id,
877
        $srrota->stockrotationstages->next->stage_id,
878
        "Adding to a rota a new sritem item being assigned to its first stage."
879
    );
880
881
    my $newrota = $builder->build({ source => 'Stockrotationrota' });
882
883
    my $srnewrota = Koha::StockRotationRotas->find($newrota->{rota_id});
884
885
    $builder->build({
886
        source => 'Stockrotationstage',
887
        value  => { rota_id => $newrota->{rota_id} },
888
    });
889
890
    $sritem->add_to_rota($newrota->{rota_id});
891
892
    is(
893
        Koha::StockRotationItems->find($item->{itemnumber})->stage_id,
894
        $srnewrota->stockrotationstages->next->stage_id,
895
        "Moving an item results in that sritem being assigned to the new first stage."
896
    );
836
897
837
    $schema->storage->txn_rollback;
898
    $schema->storage->txn_rollback;
838
};
899
};
(-)a/t/db_dependent/Koha/Libraries.t (-1 / +27 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 => 12;
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
$retrieved_category_2->delete;
57
is( Koha::LibraryCategories->search->count, $nb_of_categories + 2, 'Delete should have deleted the library category' );
58
59
# Stockrotation relationship testing
60
61
my $new_library_sr = $builder->build({ source => 'Branch' });
62
63
$builder->build({
64
    source => 'Stockrotationstage',
65
    value  => { branchcode_id => $new_library_sr->{branchcode} },
66
});
67
$builder->build({
68
    source => 'Stockrotationstage',
69
    value  => { branchcode_id => $new_library_sr->{branchcode} },
70
});
71
$builder->build({
72
    source => 'Stockrotationstage',
73
    value  => { branchcode_id => $new_library_sr->{branchcode} },
74
});
75
76
my $srstages = Koha::Libraries->find($new_library_sr->{branchcode})
77
    ->stockrotationstages;
78
is( $srstages->count, 3, 'Correctly fetched stockrotationstages associated with this branch');
79
80
isa_ok( $srstages->next, 'Koha::StockRotationStage', "Relationship correctly creates Koha::Objects." );
81
56
$schema->storage->txn_rollback;
82
$schema->storage->txn_rollback;
57
83
58
subtest '->get_effective_marcorgcode' => sub {
84
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 / +526 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
        authnotrequired => 0
55
    }
56
);
57
58
# Grab all passed data
59
# 'our' since Plack changes the scoping
60
# of 'my'
61
our %params = $input->Vars();
62
63
my $op = $params{op};
64
65
if (!defined $op) {
66
67
    # No operation is supplied, we're just displaying the list of rotas
68
    my $rotas = Koha::StockRotationRotas->search->as_list;
69
70
    $template->param(
71
        existing_rotas => $rotas,
72
        no_op_set      => 1
73
    );
74
75
} elsif ($op eq 'create_edit_rota') {
76
77
    # Edit an existing rota or define a new one
78
    my $rota_id = $params{rota_id};
79
80
    my $rota = {};
81
82
    if (!defined $rota_id) {
83
84
        # No ID supplied, we're creating a new rota
85
        # Create a shell rota hashref
86
        $rota = {
87
            cyclical => 1,
88
            active   => 0
89
        };
90
91
    } else {
92
93
        # ID supplied, we're editing an existing rota
94
        $rota = Koha::StockRotationRotas->find($rota_id);
95
96
    }
97
98
    $template->param(
99
        rota => $rota,
100
        op   => $op
101
    );
102
103
} elsif ($op eq 'toggle_rota') {
104
105
    # Find and update the active status of the rota
106
    my $rota = Koha::StockRotationRotas->find($params{rota_id});
107
108
    my $new_active = ($rota->active == 1) ? 0 : 1;
109
110
    $rota->active($new_active)->store;
111
112
    # Return to rotas page
113
    print $input->redirect('stockrotation.pl');
114
115
} elsif ($op eq 'process_rota') {
116
117
    # Get a hashref of the submitted rota data
118
    my $rota = get_rota_from_form();
119
120
    if (!process_rota($rota)) {
121
122
        # The submitted rota was invalid
123
        $template->param(
124
            error => 'invalid_form',
125
            rota => $rota,
126
            op   => 'create_edit_rota'
127
        );
128
129
    } else {
130
131
        # All was well, return to the rotas list
132
        print $input->redirect('stockrotation.pl');
133
134
    }
135
136
} elsif ($op eq 'manage_stages') {
137
138
    my $rota = Koha::StockRotationRotas->find($params{rota_id});
139
140
    $template->param(
141
        rota            => $rota,
142
        branches        => get_branches(),
143
        existing_stages => get_stages($rota),
144
        rota_id         => $params{rota_id},
145
        op              => $op
146
    );
147
148
} elsif ($op eq 'create_edit_stage') {
149
150
    # Edit an existing stage or define a new one
151
    my $stage_id = $params{stage_id};
152
153
    my $rota_id = $params{rota_id};
154
155
    if (!defined $stage_id) {
156
157
        # No ID supplied, we're creating a new stage
158
        $template->param(
159
            branches => get_branches(),
160
            stage    => {},
161
            rota_id  => $rota_id,
162
            op       => $op
163
        );
164
165
    } else {
166
167
        # ID supplied, we're editing an existing stage
168
        my $stage = Koha::StockRotationStages->find($stage_id);
169
170
        $template->param(
171
            branches => get_branches(),
172
            stage    => $stage,
173
            rota_id  => $stage->rota->rota_id,
174
            op       => $op
175
        );
176
177
    }
178
179
} elsif ($op eq 'confirm_remove_from_rota') {
180
181
    # Get the stage we're deleting
182
    $template->param(
183
        op       => $op,
184
        rota_id  => $params{rota_id},
185
        stage_id => $params{stage_id},
186
        item_id  => $params{item_id}
187
    );
188
189
} elsif ($op eq 'confirm_delete_stage') {
190
191
    # Get the stage we're deleting
192
    my $stage = Koha::StockRotationStages->find($params{stage_id});
193
194
    $template->param(
195
        op    => $op,
196
        stage => $stage
197
    );
198
199
} elsif ($op eq 'delete_stage') {
200
201
    # Get the stage we're deleting
202
    my $stage = Koha::StockRotationStages->find($params{stage_id});
203
204
    # Get the ID of the rota with which this stage is associated
205
    # (so we can return to the "Manage stages" page after deletion)
206
    my $rota_id = $stage->rota->rota_id;
207
208
    $stage->delete;
209
210
    # Return to the stages list
211
    print $input->redirect("?op=manage_stages&rota_id=$rota_id");
212
213
} elsif ($op eq 'process_stage') {
214
215
    # Get a hashref of the submitted stage data
216
    my $stage = get_stage_from_form();
217
218
    # The rota we're managing
219
    my $rota_id = $params{rota_id};
220
221
    if (!process_stage($stage, $rota_id)) {
222
223
        # The submitted stage was invalid
224
        # Get all branches
225
        my $branches = get_branches();
226
227
        $template->param(
228
            error        => 'invalid_form',
229
            all_branches => $branches,
230
            stage        => $stage,
231
            rota_id      => $rota_id,
232
            op           => 'create_edit_stage'
233
        );
234
235
    } else {
236
237
        # All was well, return to the stages list
238
        print $input->redirect("?op=manage_stages&rota_id=$rota_id");
239
240
    }
241
242
} elsif ($op eq 'manage_items') {
243
244
    my $rota = Koha::StockRotationRotas->find($params{rota_id});
245
246
    # Get all items on this rota, for each prefetch their
247
    # stage and biblio objects
248
    my $items = Koha::StockRotationItems->search(
249
        { 'stage.rota_id' => $params{rota_id} },
250
        {
251
            prefetch => {
252
                stage => {
253
                    'stockrotationitems' => {
254
                        'itemnumber' => 'biblionumber'
255
                    }
256
                }
257
            }
258
        }
259
    );
260
261
    $template->param(
262
        rota_id  => $params{rota_id},
263
        error    => $params{error},
264
        items    => $items,
265
        branches => get_branches(),
266
        stages   => get_stages($rota),
267
        rota     => $rota,
268
        op       => $op
269
    );
270
271
} elsif ($op eq 'move_to_next_stage') {
272
273
    move_to_next_stage($params{item_id}, $params{stage_id});
274
275
    # Return to the items list
276
    print $input->redirect("?op=manage_items&rota_id=" . $params{rota_id});
277
278
} elsif ($op eq 'toggle_in_demand') {
279
280
    # Toggle the item's in_demand
281
    toggle_indemand($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 'remove_item_from_stage') {
287
288
    # Remove the item from the stage
289
    remove_from_stage($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 'add_items_to_rota') {
295
296
    # The item's barcode,
297
    # which we may or may not have been passed
298
    my $barcode = $params{barcode};
299
300
    # The rota we're adding the item to
301
    my $rota_id = $params{rota_id};
302
303
    # The uploaded file filehandle,
304
    # which we may or may not have been passed
305
    my $barcode_file = $input->upload("barcodefile");
306
307
    # We need to create an array of one or more barcodes to
308
    # insert
309
    my @barcodes = ();
310
311
    # If the barcode input box was populated, use it
312
    push @barcodes, $barcode if $barcode;
313
314
    # Only parse the uploaded file if necessary
315
    if ($barcode_file) {
316
317
        # Call binmode on the filehandle as we want to set a
318
        # UTF-8 layer on it
319
        binmode($barcode_file, ":encoding(UTF-8)");
320
        # Parse the file into an array of barcodes
321
        while (my $barcode = <$barcode_file>) {
322
            $barcode =~ s/\r/\n/g;
323
            $barcode =~ s/\n+/\n/g;
324
            my @data = split(/\n/, $barcode);
325
            push @barcodes, @data;
326
        }
327
328
    }
329
330
    # A hashref to hold the status of each barcode
331
    my $barcode_status = {
332
        ok        => [],
333
        on_other  => [],
334
        on_this   => [],
335
        not_found => []
336
    };
337
338
    # If we have something to work with, do it
339
    get_barcodes_status($rota_id, \@barcodes, $barcode_status) if (@barcodes);
340
341
    # Now we know the status of each barcode, add those that
342
    # need it
343
    if (scalar @{$barcode_status->{ok}} > 0) {
344
345
        add_items_to_rota($rota_id, $barcode_status->{ok});
346
347
    }
348
    # If we were only passed one barcode and it was successfully
349
    # added, redirect back to ourselves, we don't want to display
350
    # a report, redirect also if we were passed no barcodes
351
    if (
352
        scalar @barcodes == 0 ||
353
        (scalar @barcodes == 1 && scalar @{$barcode_status->{ok}} == 1)
354
    ) {
355
356
        print $input->redirect("?op=manage_items&rota_id=$rota_id");
357
358
    } else {
359
360
        # Report on the outcome
361
        $template->param(
362
            barcode_status => $barcode_status,
363
            rota_id        => $rota_id,
364
            op             => $op
365
        );
366
367
    }
368
369
} elsif ($op eq 'move_items_to_rota') {
370
371
    # The barcodes of the items we're moving
372
    my @move = $input->param('move_item');
373
374
    foreach my $item(@move) {
375
376
        # The item we're moving
377
        my $item = Koha::Items->find($item);
378
379
        # Move it to the new rota
380
        $item->add_to_rota($params{rota_id});
381
382
    }
383
384
    # Return to the items list
385
    print $input->redirect("?op=manage_items&rota_id=".$params{rota_id});
386
387
}
388
389
output_html_with_http_headers $input, $cookie, $template->output;
390
391
sub get_rota_from_form {
392
393
    return {
394
        id          => $params{id},
395
        title       => $params{title},
396
        cyclical    => $params{cyclical},
397
        active      => $params{active},
398
        description => $params{description}
399
    };
400
}
401
402
sub get_stage_from_form {
403
404
    return {
405
        stage_id    => $params{stage_id},
406
        branchcode  => $params{branchcode},
407
        duration    => $params{duration}
408
    };
409
}
410
411
sub process_rota {
412
413
    my $sub_rota = shift;
414
415
    # Fields we require
416
    my @required = ('title','cyclical','active');
417
418
    # Count of the number of required fields we have
419
    my $valid = 0;
420
421
    # Ensure we have everything we require
422
    foreach my $req(@required) {
423
424
        if (exists $sub_rota->{$req}) {
425
426
            chomp(my $value = $sub_rota->{$req});
427
            if (length $value > 0) {
428
                $valid++;
429
            }
430
431
        }
432
433
    }
434
435
    # If we don't have everything we need
436
    return 0 if $valid != scalar @required;
437
438
    # Passed validation
439
    # Find the rota we're updating
440
    my $rota = Koha::StockRotationRotas->find($sub_rota->{id});
441
442
    if ($rota) {
443
444
        $rota->title(
445
            $sub_rota->{title}
446
        )->cyclical(
447
            $sub_rota->{cyclical}
448
        )->active(
449
            $sub_rota->{active}
450
        )->description(
451
            $sub_rota->{description}
452
        )->store;
453
454
    } else {
455
456
        $rota = Koha::StockRotationRota->new({
457
            title       => $sub_rota->{title},
458
            cyclical    => $sub_rota->{cyclical},
459
            active      => $sub_rota->{active},
460
            description => $sub_rota->{description}
461
        })->store;
462
463
    }
464
465
    return 1;
466
}
467
468
sub process_stage {
469
470
    my ($sub_stage, $rota_id) = @_;
471
472
    # Fields we require
473
    my @required = ('branchcode','duration');
474
475
    # Count of the number of required fields we have
476
    my $valid = 0;
477
478
    # Ensure we have everything we require
479
    foreach my $req(@required) {
480
481
        if (exists $sub_stage->{$req}) {
482
483
            chomp(my $value = $sub_stage->{$req});
484
            if (length $value > 0) {
485
                $valid++;
486
            }
487
488
        }
489
490
    }
491
492
    # If we don't have everything we need
493
    return 0 if $valid != scalar @required;
494
495
    # Passed validation
496
    # Find the stage we're updating
497
    my $stage = Koha::StockRotationStages->find($sub_stage->{stage_id});
498
499
    if ($stage) {
500
501
        # Updating an existing stage
502
        $stage->branchcode_id(
503
            $sub_stage->{branchcode}
504
        )->duration(
505
            $sub_stage->{duration}
506
        )->store;
507
508
    } else {
509
510
        # Creating a new stage
511
        $stage = Koha::StockRotationStage->new({
512
            branchcode_id  => $sub_stage->{branchcode},
513
            rota_id        => $rota_id,
514
            duration       => $sub_stage->{duration}
515
        })->store;
516
517
    }
518
519
    return 1;
520
}
521
522
=head1 AUTHOR
523
524
Andrew Isherwood <andrew.isherwood@ptfs-europe.com>
525
526
=cut

Return to bug 11897