Bugzilla – Attachment 70524 Details for
Bug 11897
Stock Rotation for Koha
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 11897: Add bulk of Stockrotation module.
Bug-11897-Add-bulk-of-Stockrotation-module.patch (text/plain), 177.11 KB, created by
Alex Sassmannshausen
on 2018-01-15 18:53:05 UTC
(
hide
)
Description:
Bug 11897: Add bulk of Stockrotation module.
Filename:
MIME Type:
Creator:
Alex Sassmannshausen
Created:
2018-01-15 18:53:05 UTC
Size:
177.11 KB
patch
obsolete
>From 52898b6a7bec3abd76ab887f8c390d4d667bd9d1 Mon Sep 17 00:00:00 2001 >From: Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> >Date: Thu, 8 Dec 2016 18:32:04 +0100 >Subject: [PATCH] Bug 11897: Add bulk of Stockrotation module. > >This commit replaces a set of about 20 commits into one large >commit. This makes working with the module way easier from bugzilla. > >As it stands the patches on this bug provide a fully functioning stock >rotation feature set for Koha. > >All previous patches that added functionality were signed off, so I'm >retaining that flag. > >Signed-off-by: Kathleen Milne <kathleen.milne@cne-siar.gov.uk> >--- > Koha/Item.pm | 49 +- > Koha/Library.pm | 15 + > Koha/REST/V1/Stage.pm | 48 ++ > Koha/StockRotationItem.pm | 273 +++++++++++ > Koha/StockRotationItems.pm | 124 +++++ > Koha/StockRotationRota.pm | 182 +++++++ > Koha/StockRotationRotas.pm | 101 ++++ > Koha/StockRotationStage.pm | 413 ++++++++++++++++ > Koha/StockRotationStages.pm | 86 ++++ > Koha/Util/StockRotation.pm | 247 ++++++++++ > api/v1/swagger/paths.json | 3 + > api/v1/swagger/paths/rotas.json | 79 ++++ > catalogue/stockrotation.pl | 175 +++++++ > .../data/mysql/atomicupdate/stockrot_tables.sql | 6 +- > installer/data/mysql/userflags.sql | 2 +- > installer/data/mysql/userpermissions.sql | 4 +- > koha-tmpl/intranet-tmpl/prog/css/staff-global.css | 130 ++++- > .../prog/en/includes/biblio-view-menu.inc | 1 + > .../intranet-tmpl/prog/en/includes/permissions.inc | 2 + > .../prog/en/includes/stockrotation-toolbar.inc | 13 + > .../intranet-tmpl/prog/en/includes/tools-menu.inc | 22 + > .../en/modules/admin/preferences/circulation.pref | 4 +- > .../prog/en/modules/catalogue/stockrotation.tt | 157 ++++++ > .../prog/en/modules/tools/stockrotation.tt | 482 +++++++++++++++++++ > .../prog/en/modules/tools/tools-home.tt | 7 + > .../intranet-tmpl/prog/js/pages/stockrotation.js | 65 +++ > misc/cronjobs/stockrotation.pl | 489 +++++++++++++++++++ > t/db_dependent/Items.t | 65 ++- > t/db_dependent/Koha/Libraries.t | 25 +- > t/db_dependent/StockRotationItems.t | 393 +++++++++++++++ > t/db_dependent/StockRotationRotas.t | 175 +++++++ > t/db_dependent/StockRotationStages.t | 377 +++++++++++++++ > t/db_dependent/api/v1/stockrotationstage.t | 172 +++++++ > tools/stockrotation.pl | 526 +++++++++++++++++++++ > 34 files changed, 4900 insertions(+), 12 deletions(-) > create mode 100644 Koha/REST/V1/Stage.pm > create mode 100644 Koha/StockRotationItem.pm > create mode 100644 Koha/StockRotationItems.pm > create mode 100644 Koha/StockRotationRota.pm > create mode 100644 Koha/StockRotationRotas.pm > create mode 100644 Koha/StockRotationStage.pm > create mode 100644 Koha/StockRotationStages.pm > create mode 100644 Koha/Util/StockRotation.pm > create mode 100644 api/v1/swagger/paths/rotas.json > create mode 100755 catalogue/stockrotation.pl > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/stockrotation-toolbar.inc > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/stockrotation.tt > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/tools/stockrotation.tt > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/pages/stockrotation.js > create mode 100755 misc/cronjobs/stockrotation.pl > create mode 100644 t/db_dependent/StockRotationItems.t > create mode 100644 t/db_dependent/StockRotationRotas.t > create mode 100644 t/db_dependent/StockRotationStages.t > create mode 100644 t/db_dependent/api/v1/stockrotationstage.t > create mode 100755 tools/stockrotation.pl > >diff --git a/Koha/Item.pm b/Koha/Item.pm >index bea05f8317..cba8d5adf6 100644 >--- a/Koha/Item.pm >+++ b/Koha/Item.pm >@@ -30,6 +30,8 @@ use Koha::IssuingRules; > use Koha::Item::Transfer; > use Koha::Patrons; > use Koha::Libraries; >+use Koha::StockRotationItem; >+use Koha::StockRotationRotas; > > use base qw(Koha::Object); > >@@ -235,7 +237,52 @@ sub current_holds { > return Koha::Holds->_new_from_dbic($hold_rs); > } > >-=head3 type >+=head3 stockrotationitem >+ >+ my $sritem = Koha::Item->stockrotationitem; >+ >+Returns the stock rotation item associated with the current item. >+ >+=cut >+ >+sub stockrotationitem { >+ my ( $self ) = @_; >+ my $rs = $self->_result->stockrotationitem; >+ return 0 if !$rs; >+ return Koha::StockRotationItem->_new_from_dbic( $rs ); >+} >+ >+=head3 add_to_rota >+ >+ my $item = $item->add_to_rota($rota_id); >+ >+Add this item to the rota identified by $ROTA_ID, which means associating it >+with the first stage of that rota. Should this item already be associated >+with a rota, then we will move it to the new rota. >+ >+=cut >+ >+sub add_to_rota { >+ my ( $self, $rota_id ) = @_; >+ Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber); >+ return $self; >+} >+ >+=head3 biblio >+ >+ my $biblio = $item->biblio; >+ >+Returns the biblio associated with the current item. >+ >+=cut >+ >+sub biblio { >+ my ( $self ) = @_; >+ my $rs = $self->_result->biblio; >+ return Koha::Biblio->_new_from_dbic( $rs ); >+} >+ >+=head3 _type > > =cut > >diff --git a/Koha/Library.pm b/Koha/Library.pm >index 151f30e56a..906f206932 100644 >--- a/Koha/Library.pm >+++ b/Koha/Library.pm >@@ -24,6 +24,7 @@ use Carp; > use C4::Context; > > use Koha::Database; >+use Koha::StockRotationStages; > > use base qw(Koha::Object); > >@@ -72,6 +73,20 @@ sub add_to_categories { > } > } > >+=head3 stockrotationstages >+ >+ my $stages = Koha::Library->stockrotationstages; >+ >+Returns the stockrotation stages associated with this Library. >+ >+=cut >+ >+sub stockrotationstages { >+ my ( $self ) = @_; >+ my $rs = $self->_result->stockrotationstages; >+ return Koha::StockRotationStages->_new_from_dbic( $rs ); >+} >+ > =head3 get_effective_marcorgcode > > my $marcorgcode = Koha::Libraries->find( $library_id )->get_effective_marcorgcode(); >diff --git a/Koha/REST/V1/Stage.pm b/Koha/REST/V1/Stage.pm >new file mode 100644 >index 0000000000..fa4ca7ad2b >--- /dev/null >+++ b/Koha/REST/V1/Stage.pm >@@ -0,0 +1,48 @@ >+package Koha::REST::V1::Stage; >+ >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Mojo::Base 'Mojolicious::Controller'; >+ >+use Koha::StockRotationRotas; >+use Koha::StockRotationStages; >+ >+sub move { >+ my $c = shift->openapi->valid_input or return; >+ my $input = $c->validation->output; >+ >+ my $rota = Koha::StockRotationRotas->find( $input->{rota_id} ); >+ my $stage = Koha::StockRotationStages->find( $input->{stage_id} ); >+ >+ if ( $stage && $rota ) { >+ my $result = $stage->move_to( $input->{position} ); >+ return $c->render( openapi => {}, status => 200 ) if $result; >+ return $c->render( >+ openapi => { error => "Bad request - new position invalid" }, >+ status => 400 >+ ); >+ } >+ else { >+ return $c->render( >+ openapi => { error => "Not found - Invalid rota or stage ID" }, >+ status => 404 >+ ); >+ } >+} >+ >+1; >diff --git a/Koha/StockRotationItem.pm b/Koha/StockRotationItem.pm >new file mode 100644 >index 0000000000..e451bb08db >--- /dev/null >+++ b/Koha/StockRotationItem.pm >@@ -0,0 +1,273 @@ >+package Koha::StockRotationItem; >+ >+# Copyright PTFS Europe 2016 >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use DateTime; >+use DateTime::Duration; >+use Koha::Database; >+use Koha::DateUtils qw/dt_from_string/; >+use Koha::Item::Transfer; >+use Koha::Item; >+use Koha::StockRotationStage; >+ >+use base qw(Koha::Object); >+ >+=head1 NAME >+ >+StockRotationItem - Koha StockRotationItem Object class >+ >+=head1 SYNOPSIS >+ >+StockRotationItem class used primarily by stockrotation .pls and the stock >+rotation cron script. >+ >+=head1 DESCRIPTION >+ >+Standard Koha::Objects definitions, and additional methods. >+ >+=head1 API >+ >+=head2 Class Methods >+ >+=cut >+ >+=head3 _type >+ >+=cut >+ >+sub _type { >+ return 'Stockrotationitem'; >+} >+ >+=head3 itemnumber >+ >+ my $item = Koha::StockRotationItem->itemnumber; >+ >+Returns the item associated with the current stock rotation item. >+ >+=cut >+ >+sub itemnumber { >+ my ( $self ) = @_; >+ my $rs = $self->_result->itemnumber; >+ return Koha::Item->_new_from_dbic( $rs ); >+} >+ >+=head3 stage >+ >+ my $stage = Koha::StockRotationItem->stage; >+ >+Returns the stage associated with the current stock rotation item. >+ >+=cut >+ >+sub stage { >+ my ( $self ) = @_; >+ my $rs = $self->_result->stage; >+ return Koha::StockRotationStage->_new_from_dbic( $rs ); >+} >+ >+=head3 needs_repatriating >+ >+ 1|0 = $item->needs_repatriating; >+ >+Return 1 if this item is currently not at the library it should be at >+according to our stockrotation plan. >+ >+=cut >+ >+sub needs_repatriating { >+ my ( $self ) = @_; >+ my ( $item, $stage ) = ( $self->itemnumber, $self->stage ); >+ if ( $self->itemnumber->get_transfer ) { >+ return 0; # We're in transit. >+ } elsif ( $item->holdingbranch ne $stage->branchcode_id >+ || $item->homebranch ne $stage->branchcode_id ) { >+ return 1; # We're not where we should be. >+ } else { >+ return 0; # We're at home. >+ } >+} >+ >+=head3 needs_advancing >+ >+ 1|0 = $item->needs_advancing; >+ >+Return 1 if this item is ready to be moved on to the next stage in its rota. >+ >+=cut >+ >+sub needs_advancing { >+ my ( $self ) = @_; >+ return 0 if $self->itemnumber->get_transfer; # intransfer: don't advance. >+ return 1 if $self->fresh; # Just on rota: advance. >+ my $completed = $self->itemnumber->_result->branchtransfers->search( >+ { 'comments' => "StockrotationAdvance" }, >+ { order_by => { -desc => 'datearrived' } } >+ ); >+ # Do maths on whether we need to be moved on. >+ if ( $completed->count ) { >+ my $arrival = dt_from_string( >+ $completed->next->datearrived, 'iso' >+ ); >+ my $duration = DateTime::Duration >+ ->new( days => $self->stage->duration ); >+ if ( $arrival + $duration le DateTime->now ) { >+ return 1; >+ } else { >+ return 0; >+ } >+ } else { >+ die "We have no historical branch transfer; this should not have happened!"; >+ } >+} >+ >+=head3 repatriate >+ >+ 1|0 = $sritem->repatriate >+ >+Put this item into branch transfer with 'StockrotationCorrection' comment, so >+that it may return to it's stage.branch to continue its rota as normal. >+ >+=cut >+ >+sub repatriate { >+ my ( $self, $msg ) = @_; >+ # Create the transfer. >+ my $transfer_stored = Koha::Item::Transfer->new({ >+ 'itemnumber' => $self->itemnumber_id, >+ 'frombranch' => $self->itemnumber->holdingbranch, >+ 'tobranch' => $self->stage->branchcode_id, >+ 'datesent' => DateTime->now, >+ 'comments' => $msg || "StockrotationRepatriation", >+ })->store; >+ $self->itemnumber->homebranch($self->stage->branchcode_id)->store; >+ return $transfer_stored; >+} >+ >+=head3 advance >+ >+ 1|0 = $sritem->advance; >+ >+Put this item into branch transfer with 'StockrotationAdvance' comment, to >+transfer it to the next stage in its rota. >+ >+If this is the last stage in the rota and this rota is cyclical, we return to >+the first stage. If it is not cyclical, then we delete this >+StockRotationItem. >+ >+If this item is 'indemand', and advance is invoked, we disable 'indemand' and >+advance the item as per usual. >+ >+=cut >+ >+sub advance { >+ my ( $self ) = @_; >+ my $item = $self->itemnumber; >+ my $transfer = Koha::Item::Transfer->new({ >+ 'itemnumber' => $self->itemnumber_id, >+ 'frombranch' => $item->holdingbranch, >+ 'datesent' => DateTime->now, >+ 'comments' => "StockrotationAdvance" >+ }); >+ >+ if ( $self->indemand && !$self->fresh ) { >+ $self->indemand(0)->store; # De-activate indemand >+ $transfer->tobranch($self->stage->branchcode_id); >+ $transfer->datearrived(DateTime->now); >+ } else { >+ # Find and update our stage. >+ my $stage = $self->stage; >+ my $new_stage; >+ if ( $self->fresh ) { # Just added to rota >+ $new_stage = $self->stage->first_sibling || $self->stage; >+ $transfer->tobranch($new_stage->branchcode_id); >+ $transfer->datearrived(DateTime->now) # Already at first branch >+ if $item->holdingbranch eq $new_stage->branchcode_id; >+ $self->fresh(0)->store; # Reset fresh >+ } elsif ( !$stage->last_sibling ) { # Last stage >+ if ( $stage->rota->cyclical ) { # Cyclical rota? >+ # Revert to first stage. >+ $new_stage = $stage->first_sibling || $stage; >+ $transfer->tobranch($new_stage->branchcode_id); >+ $transfer->datearrived(DateTime->now); >+ } else { >+ $self->delete; # StockRotationItem is done. >+ return 1; >+ } >+ } else { >+ # Just advance. >+ $new_stage = $self->stage->next_sibling; >+ } >+ $self->stage_id($new_stage->stage_id)->store; # Set new stage >+ $item->homebranch($new_stage->branchcode_id)->store; # Update homebranch >+ $transfer->tobranch($new_stage->branchcode_id); # Send to new branch >+ } >+ >+ return $transfer->store; >+} >+ >+=head3 investigate >+ >+ my $report = $item->investigate; >+ >+Return the base set of information, namely this individual item's report, for >+generating stockrotation reports about this stockrotationitem. >+ >+=cut >+ >+sub investigate { >+ my ( $self ) = @_; >+ my $item_report = { >+ title => $self->itemnumber->_result->biblioitem >+ ->biblionumber->title, >+ author => $self->itemnumber->_result->biblioitem >+ ->biblionumber->author, >+ callnumber => $self->itemnumber->itemcallnumber, >+ location => $self->itemnumber->location, >+ onloan => $self->itemnumber->onloan, >+ barcode => $self->itemnumber->barcode, >+ itemnumber => $self->itemnumber_id, >+ branch => $self->itemnumber->_result->holdingbranch, >+ object => $self, >+ }; >+ my $reason; >+ if ( $self->fresh ) { >+ $reason = 'initiation'; >+ } elsif ( $self->needs_repatriating ) { >+ $reason = 'repatriation'; >+ } elsif ( $self->needs_advancing ) { >+ $reason = 'advancement'; >+ $reason = 'in-demand' if $self->indemand; >+ } else { >+ $reason = 'not-ready'; >+ } >+ $item_report->{reason} = $reason; >+ >+ return $item_report; >+} >+ >+1; >+ >+=head1 AUTHOR >+ >+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> >+ >+=cut >diff --git a/Koha/StockRotationItems.pm b/Koha/StockRotationItems.pm >new file mode 100644 >index 0000000000..8fde691ae4 >--- /dev/null >+++ b/Koha/StockRotationItems.pm >@@ -0,0 +1,124 @@ >+package Koha::StockRotationItems; >+ >+# Copyright PTFS Europe 2016 >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Koha::Database; >+use Koha::StockRotationItem; >+ >+use base qw(Koha::Objects); >+ >+=head1 NAME >+ >+StockRotationItems - Koha StockRotationItems Object class >+ >+=head1 SYNOPSIS >+ >+StockRotationItems class used primarily by stockrotation .pls and the stock >+rotation cron script. >+ >+=head1 DESCRIPTION >+ >+Standard Koha::Objects definitions, and additional methods. >+ >+=head1 API >+ >+=head2 Class Methods >+ >+=cut >+ >+=head3 _type >+ >+=cut >+ >+sub _type { >+ return 'Stockrotationitem'; >+} >+ >+sub object_class { >+ return 'Koha::StockRotationItem'; >+} >+ >+=head3 investigate >+ >+ my $report = $items->investigate; >+ >+Return a stockrotation report about this set of stockrotationitems. >+ >+In this part of the overall investigation process we split individual item >+reports into appropriate action segments of our items report and increment >+some counters. >+ >+The report generated here will be used on the stage level to slot our item >+reports into appropriate sections of the branched report. >+ >+For details of intent and context of this procedure, please see >+Koha::StockRotationRota->investigate. >+ >+=cut >+ >+sub investigate { >+ my ( $self ) = @_; >+ >+ my $items_report = { >+ items => [], >+ log => [], >+ initiable_items => [], >+ repatriable_items => [], >+ advanceable_items => [], >+ indemand_items => [], >+ actionable => 0, >+ stationary => 0, >+ }; >+ while ( my $item = $self->next ) { >+ my $report = $item->investigate; >+ if ( $report->{reason} eq 'initiation' ) { >+ $items_report->{initiable}++; >+ $items_report->{actionable}++; >+ push @{$items_report->{items}}, $report; >+ push @{$items_report->{initiable_items}}, $report; >+ } elsif ( $report->{reason} eq 'repatriation' ) { >+ $items_report->{repatriable}++; >+ $items_report->{actionable}++; >+ push @{$items_report->{items}}, $report; >+ push @{$items_report->{repatriable_items}}, $report; >+ } elsif ( $report->{reason} eq 'advancement' ) { >+ $items_report->{actionable}++; >+ push @{$items_report->{items}}, $report; >+ push @{$items_report->{advanceable_items}}, $report; >+ } elsif ( $report->{reason} eq 'in-demand' ) { >+ $items_report->{actionable}++; >+ push @{$items_report->{items}}, $report; >+ push @{$items_report->{indemand_items}}, $report; >+ } else { >+ $items_report->{stationary}++; >+ push @{$items_report->{log}}, $report; >+ } >+ } >+ >+ return $items_report; >+} >+ >+1; >+ >+=head1 AUTHOR >+ >+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> >+ >+=cut >diff --git a/Koha/StockRotationRota.pm b/Koha/StockRotationRota.pm >new file mode 100644 >index 0000000000..fdf52fccd9 >--- /dev/null >+++ b/Koha/StockRotationRota.pm >@@ -0,0 +1,182 @@ >+package Koha::StockRotationRota; >+ >+# Copyright PTFS Europe 2016 >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Koha::Database; >+use Koha::StockRotationStages; >+use Koha::StockRotationItem; >+use Koha::StockRotationItems; >+ >+use base qw(Koha::Object); >+ >+=head1 NAME >+ >+StockRotationRota - Koha StockRotationRota Object class >+ >+=head1 SYNOPSIS >+ >+StockRotationRota class used primarily by stockrotation .pls and the stock >+rotation cron script. >+ >+=head1 DESCRIPTION >+ >+Standard Koha::Objects definitions, and additional methods. >+ >+=head1 API >+ >+=head2 Class Methods >+ >+=cut >+ >+=head3 stockrotationstages >+ >+ my $stages = Koha::StockRotationRota->stockrotationstages; >+ >+Returns the stages associated with the current rota. >+ >+=cut >+ >+sub stockrotationstages { >+ my ( $self ) = @_; >+ my $rs = $self->_result->stockrotationstages; >+ return Koha::StockRotationStages->_new_from_dbic( $rs ); >+} >+ >+=head3 add_item >+ >+ my $rota = $rota->add_item($itemnumber); >+ >+Add item identified by $ITEMNUMBER to this rota, which means we associate it >+with the first stage of this rota. Should the item already be associated with >+a rota, move it from that rota to this rota. >+ >+=cut >+ >+sub add_item { >+ my ( $self, $itemnumber ) = @_; >+ my $sritem = Koha::StockRotationItems->find($itemnumber); >+ if ($sritem) { >+ $sritem->stage_id($self->first_stage->stage_id) >+ ->indemand(0)->fresh(1)->store; >+ } else { >+ $sritem = Koha::StockRotationItem->new({ >+ itemnumber_id => $itemnumber, >+ stage_id => $self->first_stage->stage_id, >+ indemand => 0, >+ fresh => 1, >+ })->store; >+ } >+ return $self; >+} >+ >+=head3 first_stage >+ >+ my $stage = $rota->first_stage; >+ >+Return the first stage attached to this rota (the one that has an undefined >+`stagebefore`). >+ >+=cut >+ >+sub first_stage { >+ my ( $self ) = @_; >+ my $guess = $self->stockrotationstages->next; >+ my $stage = $guess->first_sibling; >+ return ( $stage ) ? $stage : $guess; >+} >+ >+=head3 stockrotationitems >+ >+ my $items = $rota->stockrotationitems; >+ >+Return all items associated with this rota via its stages. >+ >+=cut >+ >+sub stockrotationitems { >+ my ( $self ) = @_; >+ my $rs = Koha::StockRotationItems->search( >+ { 'stage.rota_id' => $self->rota_id }, { join => [ qw/stage/ ] } >+ ); >+ return $rs; >+} >+ >+=head3 investigate >+ >+ my $report = $rota->investigate($report_so_far); >+ >+Aim here is to return $report augmented with content for this rota. We >+delegate to $stage->investigate. >+ >+The report will include some basic information and 2 primary reports: >+ >+- per rota report in 'rotas'. This report is mainly used by admins to do check >+ & compare results. >+ >+- branched report in 'branched'. This is the workhorse: emails to libraries >+ are compiled from these reports, and they will have the actionable work. >+ >+Both reports are generated in stage based investigations; the rota report is >+then glued into place at this stage. >+ >+=cut >+ >+sub investigate { >+ my ( $self, $report ) = @_; >+ my $count = $self->stockrotationitems->count; >+ $report->{sum_items} += $count; >+ >+ if ( $self->active ) { >+ $report->{rotas_active}++; >+ # stockrotationstages->investigate augments $report with the stage's >+ # content. This is how 'branched' slowly accumulates all items. >+ $report = $self->stockrotationstages->investigate($report); >+ # Add our rota report to the full report. >+ push @{$report->{rotas}}, { >+ name => $self->title, >+ id => $self->rota_id, >+ items => $report->{tmp_items} || [], >+ log => $report->{tmp_log} || [], >+ }; >+ delete $report->{tmp_items}; >+ delete $report->{tmp_log}; >+ } else { # Rota is not active. >+ $report->{rotas_inactive}++; >+ $report->{items_inactive} += $count; >+ } >+ >+ return $report; >+} >+ >+=head3 _type >+ >+=cut >+ >+sub _type { >+ return 'Stockrotationrota'; >+} >+ >+1; >+ >+=head1 AUTHOR >+ >+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> >+ >+=cut >diff --git a/Koha/StockRotationRotas.pm b/Koha/StockRotationRotas.pm >new file mode 100644 >index 0000000000..c308a9246b >--- /dev/null >+++ b/Koha/StockRotationRotas.pm >@@ -0,0 +1,101 @@ >+package Koha::StockRotationRotas; >+ >+# Copyright PTFS Europe 2016 >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Koha::Database; >+use Koha::StockRotationRota; >+ >+use base qw(Koha::Objects); >+ >+=head1 NAME >+ >+StockRotationRotas - Koha StockRotationRotas Object class >+ >+=head1 SYNOPSIS >+ >+StockRotationRotas class used primarily by stockrotation .pls and the stock >+rotation cron script. >+ >+=head1 DESCRIPTION >+ >+Standard Koha::Objects definitions, and additional methods. >+ >+=head1 API >+ >+=head2 Class Methods >+ >+=cut >+ >+=head3 investigate >+ >+ my $report = $rotas->investigate; >+ >+Return a report detailing the current status and required actions for all >+relevant items spread over rotas. >+ >+See Koha::StockRotationRota->investigate for details. >+ >+=cut >+ >+sub investigate { >+ my ( $self ) = @_; >+ >+ my $report = { >+ actionable => 0, >+ advanceable => 0, >+ initiable => 0, >+ items_inactive => 0, >+ repatriable => 0, >+ rotas_active => 0, >+ rotas_inactive => 0, >+ stationary => 0, >+ sum_items => 0, >+ sum_rotas => $self->count, >+ branched => {}, >+ rotas => [], >+ items => [], >+ }; >+ >+ while ( my $rota = $self->next ) { >+ $report = $rota->investigate($report) >+ } >+ >+ return $report; >+} >+ >+=head3 _type >+ >+=cut >+ >+sub _type { >+ return 'Stockrotationrota'; >+} >+ >+sub object_class { >+ return 'Koha::StockRotationRota'; >+} >+ >+1; >+ >+=head1 AUTHOR >+ >+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> >+ >+=cut >diff --git a/Koha/StockRotationStage.pm b/Koha/StockRotationStage.pm >new file mode 100644 >index 0000000000..eadb71686e >--- /dev/null >+++ b/Koha/StockRotationStage.pm >@@ -0,0 +1,413 @@ >+package Koha::StockRotationStage; >+ >+# Copyright PTFS Europe 2016 >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Koha::Database; >+use Koha::Library; >+use Koha::StockRotationRota; >+ >+use base qw(Koha::Object); >+ >+=head1 NAME >+ >+StockRotationStage - Koha StockRotationStage Object class >+ >+=head1 SYNOPSIS >+ >+StockRotationStage class used primarily by stockrotation .pls and the stock >+rotation cron script. >+ >+=head1 DESCRIPTION >+ >+Standard Koha::Objects definitions, and additional methods. >+ >+=head1 API >+ >+=head2 Class Methods >+ >+=cut >+ >+=head3 _type >+ >+=cut >+ >+sub _type { >+ return 'Stockrotationstage'; >+} >+ >+sub _relation { >+ my ( $self, $method, $type ) = @_; >+ return sub { >+ my $rs = $self->_result->$method; >+ return 0 if !$rs; >+ my $namespace = 'Koha::' . $type; >+ return $namespace->_new_from_dbic( $rs ); >+ } >+} >+ >+=head3 stockrotationitems >+ >+ my $stages = Koha::StockRotationStage->stockrotationitems; >+ >+Returns the items associated with the current stage. >+ >+=cut >+ >+sub stockrotationitems { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ stockrotationitems StockRotationItems /)}; >+} >+ >+=head3 branchcode >+ >+ my $branch = Koha::StockRotationStage->branchcode; >+ >+Returns the branch associated with the current stage. >+ >+=cut >+ >+sub branchcode { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ branchcode Library /)}; >+} >+ >+=head3 rota >+ >+ my $rota = Koha::StockRotationStage->rota; >+ >+Returns the rota associated with the current stage. >+ >+=cut >+ >+sub rota { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ rota StockRotationRota /)}; >+} >+ >+=head3 siblings >+ >+ my $siblings = $stage->siblings; >+ >+Koha::Object wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub siblings { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ siblings StockRotationStages /)}; >+} >+ >+=head3 next_siblings >+ >+ my $next_siblings = $stage->next_siblings; >+ >+Koha::Object wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub next_siblings { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ next_siblings StockRotationStages /)}; >+} >+ >+=head3 previous_siblings >+ >+ my $previous_siblings = $stage->previous_siblings; >+ >+Koha::Object wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub previous_siblings { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ previous_siblings StockRotationStages /)}; >+} >+ >+=head3 next_sibling >+ >+ my $next = $stage->next_sibling; >+ >+Koha::Object wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub next_sibling { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ next_sibling StockRotationStage /)}; >+} >+ >+=head3 previous_sibling >+ >+ my $previous = $stage->previous_sibling; >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub previous_sibling { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ previous_sibling StockRotationStage /)}; >+} >+ >+=head3 first_sibling >+ >+ my $first = $stage->first_sibling; >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub first_sibling { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ first_sibling StockRotationStage /)}; >+} >+ >+=head3 last_sibling >+ >+ my $last = $stage->last_sibling; >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub last_sibling { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ last_sibling StockRotationStage /)}; >+} >+ >+=head3 move_previous >+ >+ 1|0 = $stage->move_previous; >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub move_previous { >+ my ( $self ) = @_; >+ return $self->_result->move_previous; >+} >+ >+=head3 move_next >+ >+ 1|0 = $stage->move_next; >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub move_next { >+ my ( $self ) = @_; >+ return $self->_result->move_next; >+} >+ >+=head3 move_first >+ >+ 1|0 = $stage->move_first; >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub move_first { >+ my ( $self ) = @_; >+ return $self->_result->move_first; >+} >+ >+=head3 move_last >+ >+ 1|0 = $stage->move_last; >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub move_last { >+ my ( $self ) = @_; >+ return $self->_result->move_last; >+} >+ >+=head3 move_to >+ >+ 1|0 = $stage->move_to($position); >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub move_to { >+ my ( $self, $position ) = @_; >+ return $self->_result->move_to($position) >+ if ( $position le $self->rota->stockrotationstages->count ); >+ return 0; >+} >+ >+=head3 move_to_group >+ >+ 1|0 = $stage->move_to_group($rota_id, [$position]); >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub move_to_group { >+ my ( $self, $rota_id, $position ) = @_; >+ return $self->_result->move_to_group($rota_id, $position); >+} >+ >+=head3 delete >+ >+ 1|0 = $stage->delete; >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub delete { >+ my ( $self ) = @_; >+ return $self->_result->delete; >+} >+ >+=head3 investigate >+ >+ my $report = $stage->investigate($report_so_far); >+ >+Return a stage based report. This report will mutate and augment the report >+that is passed to it. It slots item reports into the branched and temporary >+rota sections of the report. It also increments a number of counters. >+ >+For details of intent and context of this procedure, please see >+Koha::StockRotationRota->investigate. >+ >+=cut >+ >+sub investigate { >+ my ( $self, $report ) = @_; >+ my $new_stage = $self->next_sibling; >+ my $duration = $self->duration; >+ # Generate stage items report >+ my $items_report = $self->stockrotationitems->investigate; >+ >+ # Merge into general report >+ >+ ## Branched indexes >+ ### The branched indexes work as follows: >+ ### - They contain information about the relevant branch >+ ### - They contain an index of actionable items for that branch >+ ### - They contain an index of non-actionable items for that branch >+ >+ ### Items are assigned to a particular branched index as follows: >+ ### - 'advanceable' : assigned to branch of the current stage >+ ### (this should also be the current holding branch) >+ ### - 'log' items are always assigned to branch of current stage. >+ ### - 'indemand' : assigned to branch of current stage >+ ### (this should also be the current holding branch) >+ ### - 'initiable' : assigned to the current holding branch of item >+ ### - 'repatriable' : assigned to the current holding branch of item >+ >+ ### 'Advanceable', 'log', 'indemand': >+ >+ # Set up our stage branch info. >+ my $stagebranch = $self->_result->branchcode; >+ my $stagebranchcode = $stagebranch->branchcode; >+ >+ # Initiate our stage branch index if it does not yet exist. >+ if ( !$report->{branched}->{$stagebranchcode} ) { >+ $report->{branched}->{$stagebranchcode} = { >+ code => $stagebranchcode, >+ name => $stagebranch->branchname, >+ email => $stagebranch->branchemail, >+ phone => $stagebranch->branchphone, >+ items => [], >+ log => [], >+ }; >+ } >+ >+ push @{$report->{branched}->{$stagebranchcode}->{items}}, >+ @{$items_report->{advanceable_items}}; >+ push @{$report->{branched}->{$stagebranchcode}->{log}}, >+ @{$items_report->{log}}; >+ push @{$report->{branched}->{$stagebranchcode}->{items}}, >+ @{$items_report->{indemand_items}}; >+ >+ ### 'Initiable' & 'Repatriable' >+ foreach my $ireport (@{$items_report->{initiable_items}}) { >+ my $branch = $ireport->{branch}; >+ my $branchcode = $branch->branchcode; >+ if ( !$report->{branched}->{$branchcode} ) { >+ $report->{branched}->{$branchcode} = { >+ code => $branchcode, >+ name => $branch->branchname, >+ email => $branch->branchemail, >+ phone => $branch->branchphone, >+ items => [], >+ log => [], >+ }; >+ } >+ push @{$report->{branched}->{$branchcode}->{items}}, $ireport; >+ } >+ >+ foreach my $ireport (@{$items_report->{repatriable_items}}) { >+ my $branch = $ireport->{branch}; >+ my $branchcode = $branch->branchcode; >+ if ( !$report->{branched}->{$branchcode} ) { >+ $report->{branched}->{$branchcode} = { >+ code => $branchcode, >+ name => $branch->branchname, >+ email => $branch->branchemail, >+ phone => $branch->branchphone, >+ items => [], >+ log => [], >+ }; >+ } >+ push @{$report->{branched}->{$branchcode}->{items}}, $ireport; >+ } >+ >+ ## Per rota indexes >+ ### Per rota indexes are item reports pushed into the index for the >+ ### current rota. We don't know where that index is yet as we don't know >+ ### about the current rota. To resolve this we assign our items and log >+ ### to tmp indexes. They will be merged into the proper rota index at the >+ ### rota level. >+ push @{$report->{tmp_items}}, @{$items_report->{items}}; >+ push @{$report->{tmp_log}}, @{$items_report->{log}}; >+ >+ ## Collection of items >+ ### Finally we just add our collection of items to the full item index. >+ push @{$report->{items}}, @{$items_report->{items}}; >+ >+ ## Assemble counters >+ $report->{actionable} += $items_report->{actionable}; >+ $report->{indemand} += scalar @{$items_report->{indemand_items}}; >+ $report->{advanceable} += scalar @{$items_report->{advanceable_items}}; >+ $report->{initiable} += scalar @{$items_report->{initiable_items}}; >+ $report->{repatriable} += scalar @{$items_report->{repatriable_items}}; >+ $report->{stationary} += scalar @{$items_report->{log}}; >+ >+ return $report; >+} >+ >+1; >+ >+=head1 AUTHOR >+ >+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> >+ >+=cut >diff --git a/Koha/StockRotationStages.pm b/Koha/StockRotationStages.pm >new file mode 100644 >index 0000000000..00faaab28d >--- /dev/null >+++ b/Koha/StockRotationStages.pm >@@ -0,0 +1,86 @@ >+package Koha::StockRotationStages; >+ >+# Copyright PTFS Europe 2016 >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Koha::Database; >+use Koha::StockRotationStage; >+ >+use base qw(Koha::Objects); >+ >+=head1 NAME >+ >+StockRotationStages - Koha StockRotationStages Object class >+ >+=head1 SYNOPSIS >+ >+StockRotationStages class used primarily by stockrotation .pls and the stock >+rotation cron script. >+ >+=head1 DESCRIPTION >+ >+Standard Koha::Objects definitions, and additional methods. >+ >+=head1 API >+ >+=head2 Class Methods >+ >+=cut >+ >+=head3 investigate >+ >+ my $report = $stages->investigate($rota_so_far); >+ >+Return a report detailing the current status and required actions for all >+relevant items spread over the set of stages. >+ >+For details of intent and context of this procedure, please see >+Koha::StockRotationRota->investigate. >+ >+=cut >+ >+sub investigate { >+ my ( $self, $report ) = @_; >+ >+ while ( my $stage = $self->next ) { >+ $report = $stage->investigate($report); >+ } >+ >+ return $report; >+} >+ >+=head3 _type >+ >+=cut >+ >+sub _type { >+ return 'Stockrotationstage'; >+} >+ >+sub object_class { >+ return 'Koha::StockRotationStage'; >+} >+ >+1; >+ >+=head1 AUTHOR >+ >+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> >+ >+=cut >diff --git a/Koha/Util/StockRotation.pm b/Koha/Util/StockRotation.pm >new file mode 100644 >index 0000000000..13c78e5675 >--- /dev/null >+++ b/Koha/Util/StockRotation.pm >@@ -0,0 +1,247 @@ >+package Koha::Util::StockRotation; >+ >+# Module contains subroutines used with Stock Rotation >+# >+# Copyright 2016 PTFS-Europe Ltd >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Koha::Items; >+use Koha::StockRotationItems; >+use Koha::Database; >+ >+our ( @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS ); >+BEGIN { >+ require Exporter; >+ @ISA = qw( Exporter ); >+ @EXPORT = qw( ); >+ @EXPORT_OK = qw( >+ get_branches >+ get_stages >+ toggle_indemand >+ remove_from_stage >+ get_barcodes_status >+ add_items_to_rota >+ move_to_next_stage >+ ); >+ %EXPORT_TAGS = ( ALL => [ @EXPORT_OK, @EXPORT ] ); >+} >+ >+=head1 NAME >+ >+Koha::Util::StockRotation - utility class with routines for Stock Rotation >+ >+=head1 FUNCTIONS >+ >+=head2 get_branches >+ >+ returns all branches ordered by branchname as an array, each element >+ contains a hashref containing branch details >+ >+=cut >+ >+sub get_branches { >+ >+ return Koha::Libraries->search( >+ {}, >+ { order_by => ['branchname'] } >+ )->unblessed; >+ >+} >+ >+=head2 get_stages >+ >+ returns an arrayref of StockRotationStage objects representing >+ all stages for a passed rota >+ >+=cut >+ >+sub get_stages { >+ >+ my $rota = shift; >+ >+ my @out = (); >+ >+ if ($rota->stockrotationstages->count > 0) { >+ >+ push @out, $rota->first_stage->unblessed; >+ >+ push @out, @{$rota->first_stage->siblings->unblessed}; >+ >+ } >+ >+ return \@out; >+} >+ >+=head2 toggle_indemand >+ >+ given an item's ID & stage ID toggle that item's in_demand >+ status on that stage >+ >+=cut >+ >+sub toggle_indemand { >+ >+ my ($item_id, $stage_id) = @_; >+ >+ # Get the item object >+ my $item = Koha::StockRotationItems->find( >+ { >+ itemnumber_id => $item_id, >+ stage_id => $stage_id >+ } >+ ); >+ >+ # Toggle the item's indemand flag >+ my $new_indemand = ($item->indemand == 1) ? 0 : 1; >+ >+ $item->indemand($new_indemand)->store; >+ >+} >+ >+=head2 move_to_next_stage >+ >+ given an item's ID and stage ID, move it >+ to the next stage on the rota >+ >+=cut >+ >+sub move_to_next_stage { >+ >+ my ($item_id, $stage_id) = shift; >+ >+ # Get the item object >+ my $item = Koha::StockRotationItems->find( >+ { >+ itemnumber_id => $item_id, >+ stage_id => $stage_id >+ } >+ ); >+ >+ $item->advance; >+ >+} >+ >+=head2 remove_from_stage >+ >+ given an item's ID & stage ID, remove that item from that stage >+ >+=cut >+ >+sub remove_from_stage { >+ >+ my ($item_id, $stage_id) = @_; >+ >+ # Get the item object and delete it >+ Koha::StockRotationItems->find( >+ { >+ itemnumber_id => $item_id, >+ stage_id => $stage_id >+ } >+ )->delete; >+ >+} >+ >+=head2 get_barcodes_status >+ >+ take an arrayref of barcodes and a status hashref and populate it >+ >+=cut >+ >+sub get_barcodes_status { >+ >+ my ($rota_id, $barcodes, $status) = @_; >+ >+ # Get the items associated with these barcodes >+ my $items = Koha::Items->search( >+ { >+ barcode => { '-in' => $barcodes } >+ }, >+ { >+ prefetch => 'stockrotationitem' >+ } >+ ); >+ # Get an array of barcodes that were found >+ # Assign each barcode's status >+ my @found = (); >+ while (my $item = $items->next) { >+ >+ push @found, $item->barcode if $item->barcode; >+ >+ # Check if it's on a rota >+ my $on_rota = $item->stockrotationitem; >+ >+ # It is on a rota >+ if ($on_rota) { >+ >+ # Check if it's on this rota >+ if ($on_rota->stage->rota->rota_id == $rota_id) { >+ >+ # It's on this rota >+ push @{$status->{on_this}}, $item; >+ >+ } else { >+ >+ # It's on another rota >+ push @{$status->{on_other}}, $item; >+ >+ } >+ >+ } else { >+ >+ # Item is not on a rota >+ push @{$status->{ok}}, $item; >+ >+ } >+ >+ } >+ >+ # Create an array of barcodes supplied in the file that >+ # were not found in the catalogue >+ my %found_in_cat = map{ $_ => 1 } @found; >+ push @{$status->{not_found}}, grep( >+ !defined $found_in_cat{$_}, @{$barcodes} >+ ); >+ >+} >+ >+=head2 add_items_to_rota >+ >+ take an arrayref of Koha::Item objects and add them to the passed rota >+ >+=cut >+ >+sub add_items_to_rota { >+ >+ my ($rota_id, $items) = @_; >+ >+ foreach my $item(@{$items}) { >+ >+ $item->add_to_rota($rota_id); >+ >+ } >+ >+} >+ >+1; >+ >+=head1 AUTHOR >+ >+Andrew Isherwood <andrew.isherwood@ptfs-europe.com> >+ >+=cut >diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json >index 9c3a6d2efd..90a99efd87 100644 >--- a/api/v1/swagger/paths.json >+++ b/api/v1/swagger/paths.json >@@ -25,5 +25,8 @@ > }, > "/illrequests": { > "$ref": "paths/illrequests.json#/~1illrequests" >+ }, >+ "/rotas/{rota_id}/stages/{stage_id}/position": { >+ "$ref": "paths/rotas.json#/~1rotas~1{rota_id}~1stages~1{stage_id}~1position" > } > } >diff --git a/api/v1/swagger/paths/rotas.json b/api/v1/swagger/paths/rotas.json >new file mode 100644 >index 0000000000..0cdda30317 >--- /dev/null >+++ b/api/v1/swagger/paths/rotas.json >@@ -0,0 +1,79 @@ >+{ >+ "/rotas/{rota_id}/stages/{stage_id}/position": { >+ "put": { >+ "x-mojo-to": "Stage#move", >+ "operationId": "moveStage", >+ "tags": ["rotas"], >+ "parameters": [{ >+ "name": "rota_id", >+ "in": "path", >+ "required": true, >+ "description": "A rotas ID", >+ "type": "integer" >+ }, { >+ "name": "stage_id", >+ "in": "path", >+ "required": true, >+ "description": "A stages ID", >+ "type": "integer" >+ }, { >+ "name": "position", >+ "in": "body", >+ "required": true, >+ "description": "A stages position in the rota", >+ "schema": { >+ "type": "integer" >+ } >+ }], >+ "produces": [ >+ "application/json" >+ ], >+ "responses": { >+ "200": { >+ "description": "OK" >+ }, >+ "400": { >+ "description": "Bad request", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "401": { >+ "description": "Authentication required", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "403": { >+ "description": "Access forbidden", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "404": { >+ "description": "Position not found", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "500": { >+ "description": "Internal server error", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "503": { >+ "description": "Under maintenance", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ } >+ }, >+ "x-koha-authorization": { >+ "permissions": { >+ "borrowers": "1" >+ } >+ } >+ } >+ } >+} >diff --git a/catalogue/stockrotation.pl b/catalogue/stockrotation.pl >new file mode 100755 >index 0000000000..ff63b8a270 >--- /dev/null >+++ b/catalogue/stockrotation.pl >@@ -0,0 +1,175 @@ >+#!/usr/bin/perl >+ >+# Copyright 2016 PTFS-Europe Ltd >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+=head1 stockrotation.pl >+ >+ Script to manage item assignments to stock rotation rotas. Including their >+ assiciated stages >+ >+=cut >+ >+use Modern::Perl; >+use CGI; >+ >+use C4::Auth; >+use C4::Output; >+use C4::Search; >+ >+use Koha::Biblio; >+use Koha::Item; >+use Koha::StockRotationRotas; >+use Koha::StockRotationStages; >+use Koha::Util::StockRotation qw(:ALL); >+ >+my $input = new CGI; >+ >+unless (C4::Context->preference('StockRotation')) { >+ # redirect to Intranet home if self-check is not enabled >+ print $input->redirect("/cgi-bin/koha/mainpage.pl"); >+ exit; >+} >+ >+my %params = $input->Vars(); >+ >+my $op = $params{op}; >+ >+my $biblionumber = $input->param('biblionumber'); >+ >+my ($template, $loggedinuser, $cookie) = get_template_and_user( >+ { >+ template_name => 'catalogue/stockrotation.tt', >+ query => $input, >+ type => 'intranet', >+ authnotrequired => 0, >+ flagsrequired => { catalogue => 1 } >+ } >+); >+ >+if (!defined $op) { >+ >+ # List all items along with their associated rotas >+ my $biblio = Koha::Biblios->find($biblionumber); >+ >+ my $items = $biblio->items; >+ >+ # Get only rotas with stages >+ my $rotas = Koha::StockRotationRotas->search( >+ { >+ 'stockrotationstages.stage_id' => { '!=', undef } >+ }, >+ { >+ join => 'stockrotationstages', >+ collapse => 1 >+ } >+ ); >+ >+ # Construct a model to pass to the view >+ my @item_data = (); >+ >+ while (my $item = $items->next) { >+ >+ my $item_hashref = { >+ bib_item => $item >+ }; >+ >+ my $stockrotationitem = $item->stockrotationitem; >+ >+ # If this item is on a rota >+ if ($stockrotationitem != 0) { >+ >+ # This item's rota >+ my $rota = $stockrotationitem->stage->rota; >+ >+ # This rota's stages >+ my $stages = get_stages($rota); >+ >+ $item_hashref->{rota} = $rota; >+ >+ $item_hashref->{stockrotationitem} = $stockrotationitem; >+ >+ $item_hashref->{stages} = $stages; >+ >+ } >+ >+ push @item_data, $item_hashref; >+ >+ } >+ >+ $template->param( >+ no_op_set => 1, >+ rotas => $rotas, >+ items => \@item_data, >+ branches => get_branches(), >+ biblio => $biblio, >+ biblionumber => $biblio->biblionumber, >+ stockrotationview => 1, >+ C4::Search::enabled_staff_search_views >+ ); >+ >+} elsif ($op eq "toggle_in_demand") { >+ >+ # Toggle in demand >+ toggle_indemand($params{item_id}, $params{stage_id}); >+ >+ # Return to items list >+ print $input->redirect("?biblionumber=$biblionumber"); >+ >+} elsif ($op eq "remove_item_from_stage") { >+ >+ # Remove from the stage >+ remove_from_stage($params{item_id}, $params{stage_id}); >+ >+ # Return to items list >+ print $input->redirect("?biblionumber=$biblionumber"); >+ >+} elsif ($op eq "move_to_next_stage") { >+ >+ move_to_next_stage($params{item_id}, $params{stage_id}); >+ >+ # Return to items list >+ print $input->redirect("?biblionumber=" . $params{biblionumber}); >+ >+} elsif ($op eq "add_item_to_rota") { >+ >+ my $item = Koha::Items->find($params{item_id}); >+ >+ $item->add_to_rota($params{rota_id}); >+ >+ print $input->redirect("?biblionumber=" . $params{biblionumber}); >+ >+} elsif ($op eq "confirm_remove_from_rota") { >+ >+ $template->param( >+ op => $params{op}, >+ stage_id => $params{stage_id}, >+ item_id => $params{item_id}, >+ biblionumber => $params{biblionumber}, >+ stockrotationview => 1, >+ C4::Search::enabled_staff_search_views >+ ); >+ >+} >+ >+output_html_with_http_headers $input, $cookie, $template->output; >+ >+=head1 AUTHOR >+ >+Andrew Isherwood <andrew.isherwood@ptfs-europe.com> >+ >+=cut >diff --git a/installer/data/mysql/atomicupdate/stockrot_tables.sql b/installer/data/mysql/atomicupdate/stockrot_tables.sql >index d6cf26b003..8469d20ca7 100644 >--- a/installer/data/mysql/atomicupdate/stockrot_tables.sql >+++ b/installer/data/mysql/atomicupdate/stockrot_tables.sql >@@ -55,8 +55,8 @@ INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) V > -- Permissions > > INSERT IGNORE INTO userflags (bit, flag, flagdesc, defaulton) VALUES >- (21, 'stockrotation', 'Manage stockrotation operations', 0); >+ (23, 'stockrotation', 'Manage stockrotation operations', 0); > > INSERT IGNORE INTO permissions (module_bit, code, description) VALUES >- (21, 'can_edit_rotas', 'Create, edit and delete rotas'), >- (21, 'can_add_items_rotas', 'Add and remove items from rotas'); >+ (23, 'can_edit_rotas', 'Create, edit and delete rotas'), >+ (23, 'can_add_items_rotas', 'Add and remove items from rotas'); >diff --git a/installer/data/mysql/userflags.sql b/installer/data/mysql/userflags.sql >index 6305b0a277..72e1046cbe 100644 >--- a/installer/data/mysql/userflags.sql >+++ b/installer/data/mysql/userflags.sql >@@ -19,6 +19,6 @@ INSERT INTO userflags (bit, flag, flagdesc, defaulton) VALUES > (19, 'plugins', 'Koha plugins', '0'), > (20, 'lists', 'Lists', 0), > (21, 'clubs', 'Patron clubs', '0'), >-(22,'ill','The Interlibrary Loans Module',0) >+(22,'ill','The Interlibrary Loans Module',0), > (23, 'stockrotation', 'Manage stockrotation operations', 0) > ; >diff --git a/installer/data/mysql/userpermissions.sql b/installer/data/mysql/userpermissions.sql >index bddfbed5af..dabc2dfc5b 100644 >--- a/installer/data/mysql/userpermissions.sql >+++ b/installer/data/mysql/userpermissions.sql >@@ -82,6 +82,6 @@ INSERT INTO permissions (module_bit, code, description) VALUES > (21, 'edit_templates', 'Create and update club templates'), > (21, 'edit_clubs', 'Create and update clubs'), > (21, 'enroll', 'Enroll patrons in clubs'), >- (22, 'can_edit_rotas', 'Create, edit and delete rotas'), >- (22, 'can_add_items_rotas', 'Add and remove items from rotas') >+ (23, 'can_edit_rotas', 'Create, edit and delete rotas'), >+ (23, 'can_add_items_rotas', 'Add and remove items from rotas') > ; >diff --git a/koha-tmpl/intranet-tmpl/prog/css/staff-global.css b/koha-tmpl/intranet-tmpl/prog/css/staff-global.css >index 515c4e3c76..7c34af3054 100644 >--- a/koha-tmpl/intranet-tmpl/prog/css/staff-global.css >+++ b/koha-tmpl/intranet-tmpl/prog/css/staff-global.css >@@ -3207,4 +3207,132 @@ table#ill-requests { > .loggedin-menu-label { > display: none; > } >-} >\ No newline at end of file >+} >+ >+#stockrotation h3 { >+ margin: 30px 0 10px 0; >+} >+ >+#stockrotation .dialog h3 { >+ margin: 10px 0; >+} >+ >+#stockrotation .dialog { >+ margin-bottom: 20px; >+} >+ >+#stockrotation .highlight_stage, >+#catalog_stockrotation .highlight_stage { >+ font-weight: bold; >+} >+ >+#stockrotation #rota_form textarea { >+ width: 300px; >+ height: 100px; >+} >+ >+#stockrotation #rota_form #name { >+ width: 300px; >+} >+ >+#stockrotation #rota_form fieldset, >+#stockrotation #stage_form fieldset, >+#stockrotation #add_rota_item_form fieldset { >+ width: auto; >+} >+ >+#stockrotation .dialog.alert ul { >+ margin: 20px 0; >+} >+ >+#stockrotation .dialog.alert li { >+ list-style-type: none; >+} >+ >+#catalog_stockrotation .item_select_rota { >+ vertical-align: middle; >+} >+ >+#catalog_stockrotation h1 { >+ margin-bottom: 20px; >+} >+ >+#stockrotation td.actions, >+#catalog_stockrotation td.actions { >+ vertical-align: middle; >+} >+ >+#stockrotation .stage, >+#catalog_stockrotation .stage { >+ display: inline-block; >+ padding: 5px 7px; >+ margin: 3px 0 3px 0; >+ border-radius: 5px; >+ background-color: rgba(0,0,0,0.1); >+} >+ >+#stage_list_headings { >+ font-weight: bold; >+} >+ >+#stage_list_headings span { >+ padding: 3px; >+} >+ >+#manage_stages ul { >+ padding-left: 0; >+} >+ >+#manage_stages li { >+ list-style: none; >+ margin-bottom: 5px; >+} >+ >+#manage_stages li span { >+ padding: 6px 3px; >+} >+ >+#manage_stages .stagename { >+ width: 15em; >+ display: inline-block; >+} >+ >+#manage_stages .stageduration { >+ width: 10em; >+ display: inline-block; >+} >+ >+#manage_stages .stageactions { >+ display: inline-block; >+} >+ >+#manage_stages li:nth-child(odd) { >+ background-color : #F3F3F3; >+} >+ >+#manage_stages .drag_handle { >+ margin-right: 6px; >+ cursor: move; >+} >+ >+#manage_stages .drag_placeholder { >+ height: 2em; >+ border: 1px dotted #aaa; >+} >+ >+#manage_stages h3 { >+ display: inline-block; >+} >+ >+#manage_stages #ajax_status { >+ display: inline-block; >+ border: 1px solid #bcbcbc; >+ border-radius: 5px; >+ padding: 5px; >+ margin-left: 10px; >+ background: #f3f3f3; >+} >+ >+#manage_stages #manage_stages_help { >+ margin: 20px 0; >+} >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/biblio-view-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/biblio-view-menu.inc >index 95c0ee4640..b32f60939e 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/biblio-view-menu.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/biblio-view-menu.inc >@@ -40,6 +40,7 @@ > [% IF ( issuehistoryview ) %]<li class="active">[% ELSE %]<li>[% END %] > <a href="/cgi-bin/koha/catalogue/issuehistory.pl?biblionumber=[% biblio_object_id | url %]" >Checkout history</a></li> > [% 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&modules=CATALOGUING&action=MODIFY&object=[% biblio_object_id | url %]">Modification log</a> </li>[% END %] >+[% 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 %] > </ul> > </div> > >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc >index cc06ff32a6..816dba2737 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc >@@ -110,5 +110,7 @@ > [%- CASE 'edit_templates' -%]<span>Create and edit club templates</span> > [%- CASE 'enroll' -%]<span>Enroll patrons in clubs</span> > [%- CASE 'edi_manage' -%]<span>Manage EDIFACT transmissions</span> >+ [%- CASE 'can_add_items_rotas' -%]<span>Add and remove items from rotas</span> >+ [%- CASE 'can_edit_rotas' -%]<span>Create, edit and delete rotas</span> > [%- END -%] > [%- END -%] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/stockrotation-toolbar.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/stockrotation-toolbar.inc >new file mode 100644 >index 0000000000..c8e72e9dd6 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/stockrotation-toolbar.inc >@@ -0,0 +1,13 @@ >+[% USE Koha %] >+<div id="toolbar" class="btn-toolbar"> >+ [% IF no_op_set %] >+ <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> >+ [% END %] >+ [% IF op == 'manage_stages' %] >+ <a id="addstage" class="btn btn-default btn-sm" href="/cgi-bin/koha/tools/stockrotation.pl?op=create_edit_stage&rota_id=[% rota_id %]"><i class="fa fa-plus"></i> New stage</a> >+ [% END %] >+ [% IF op == 'manage_items' %] >+ <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> >+ <a id="editrota" class="btn btn-default btn-sm" href="/cgi-bin/koha/tools/stockrotation.pl?op=create_edit_rota&rota_id=[% rota_id %]"><i class="fa fa-pencil"></i> Edit rota</a> >+ [% END %] >+</div> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc >index 50e52f73c0..d4a6ad2b1c 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc >@@ -1,3 +1,22 @@ >+[% USE Koha %] >+ >+<script type="text/javascript">//<![CDATA[ >+ $(document).ready(function() { >+ var path = location.pathname.substring(1); >+ if (path.indexOf("labels") >= 0 && path.indexOf("spine") < 0 ) { >+ $('#navmenulist a[href$="/cgi-bin/koha/labels/label-home.pl"]').css('font-weight','bold'); >+ } else if (path.indexOf("patroncards") >= 0 ) { >+ $('#navmenulist a[href$="/cgi-bin/koha/patroncards/home.pl"]').css('font-weight','bold'); >+ } else if (path.indexOf("patron_lists") >= 0 ) { >+ $('#navmenulist a[href$="/cgi-bin/koha/patron_lists/lists.pl"]').css('font-weight','bold'); >+ } else if ((path+location.search).indexOf("batchMod.pl?del=1") >= 0 ) { >+ $('#navmenulist a[href$="/cgi-bin/koha/tools/batchMod.pl?del=1"]').css('font-weight','bold'); >+ } else { >+ $('#navmenulist a[href$="/' + path + '"]').css('font-weight','bold'); >+ } >+ }); >+//]]> >+</script> > <div id="navmenu"> > <div id="navmenulist"> > <ul> >@@ -38,6 +57,9 @@ > [% IF ( CAN_user_tools_batch_upload_patron_images ) %] > <li><a href="/cgi-bin/koha/tools/picture-upload.pl">Upload patron images</a></li> > [% END %] >+ [% IF ( CAN_user_stockrotation_can_edit_rotas && Koha.Preference('StockRotation') ) %] >+ <li><a href="/cgi-bin/koha/tools/stockrotation.pl">Stock rotation</a></li> >+ [% END %] > </ul> > <h5>Catalog</h5> > <ul> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref >index 329a228b7f..7655949f55 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref >@@ -868,7 +868,7 @@ Circulation: > yes: Enable > no: Disable > - "housebound module" >- Stockrotation module: >+ Stock Rotation module: > - > - pref: StockRotation > choices: >@@ -880,7 +880,7 @@ Circulation: > choices: > yes: Enable > no: Disable >- - "Prevent branchtransfers on items in stockrotation rotas" >+ - "Prevent branchtransfers on items in stock rotation rotas" > Article Requests: > - > - pref: ArticleRequests >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/stockrotation.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/stockrotation.tt >new file mode 100644 >index 0000000000..8d9b011d87 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/stockrotation.tt >@@ -0,0 +1,157 @@ >+[% USE Koha %] >+[% USE Branches %] >+[% INCLUDE 'doc-head-open.inc' %] >+<title>Koha › Catalog › Stock rotation details for [% biblio.title %]</title> >+[% INCLUDE 'doc-head-close.inc' %] >+[% INCLUDE 'browser-strings.inc' %] >+<!--[if lt IE 9]> >+<script type="text/javascript" src="[% interface %]/lib/shims/json2.min.js"></script> >+<![endif]--> >+<script type="text/javascript" src="[% interface %]/js/browser.js"></script> >+<script type="text/javascript"> >+//<![CDATA[ >+ var browser = KOHA.browser('[% searchid %]', parseInt('[% biblionumber %]', 10)); >+ browser.show(); >+//]]> >+</script> >+</head> >+<body id="catalog_stockrotation" class="catalog"> >+[% USE KohaDates %] >+[% INCLUDE 'header.inc' %] >+[% INCLUDE 'cat-search.inc' %] >+ >+<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> › <a href="/cgi-bin/koha/catalogue/search.pl">Catalog</a> › Stock rotation details for <i>[% biblio.title | html %][% FOREACH subtitle IN biblio.subtitles %][% subtitle.subfield %][% END %]</i></div> >+ >+<div id="doc3" class="yui-t2"> >+ >+ <div id="bd"> >+ <div id="yui-main"> >+ <div class="yui-b"> >+ >+<div id="catalogue_detail_biblio"> >+ >+ [% IF no_op_set %] >+ <h1 class="title">Stock rotation details for [% biblio.title | html %]</h1> >+ [% IF rotas.count > 0 && items.size > 0 %] >+ >+ <table class="items_table dataTable no-footer" role="grid"> >+ <thead> >+ <tr> >+ <th>Barcode</th> >+ <th>Shelfmark</th> >+ <th>Rota</th> >+ <th>Rota status</th> >+ <th>In transit</th> >+ <th>Stages & duration in days<br>(current stage highlighted)</th> >+ <th> </th> >+ </tr> >+ </thead> >+ <tbody> >+ [% FOREACH item IN items %] >+ <tr> >+ <td>[% item.bib_item.barcode %]</td> >+ <td>[% item.bib_item.itemcallnumber %]</td> >+ <td> >+ [% item.rota.title %] >+ </td> >+ <td> >+ [% IF item.rota %] >+ [% IF !item.rota.active %] >+ <span class="highlighted-row"> >+ [% END %] >+ [% item.rota.active ? 'Active' : 'Inactive' %] >+ >+ [% IF !item.rota.active %] >+ </span> >+ [% END %] >+ [% END %] >+ </td> >+ <td> >+ [% item.bib_item.get_transfer ? 'Yes' : 'No' %] >+ </td> >+ <td> >+ [% FOREACH this_stage IN item.stages %] >+ [% IF this_stage.stage_id == item.stockrotationitem.stage.stage_id %] >+ <span class="stage highlight_stage"> >+ [% ELSE %] >+ <span class="stage"> >+ [% END %] >+ [% Branches.GetName(this_stage.branchcode_id) %] ([% this_stage.duration %]) >+ </span> >+ » >+ [% END %] >+ [% IF item.stages.size > 0 %] >+ <span class="stage">[% item.rota.cyclical ? 'START' : 'END' %]</span> >+ [% END %] >+ </td> >+ <td class="actions"> >+ [% IF item.stockrotationitem %] >+ [% in_transit = item.bib_item.get_transfer %] >+ [% IF !in_transit && item.stages.size > 1 %] >+ <a class="btn btn-default btn-xs" href="?op=move_to_next_stage&stage_id=[% item.stockrotationitem.stage.stage_id %]&item_id=[% item.bib_item.id %]&biblionumber=[% biblionumber %]"> >+ [% ELSE %] >+ <a class="btn btn-default btn-xs" disabled> >+ [% END %] >+ <i class="fa fa-arrow-right"></i> >+ Move to next stage >+ </a> >+ [% IF !in_transit %] >+ <a class="btn btn-default btn-xs" href="?op=toggle_in_demand&stage_id=[% item.stockrotationitem.stage.stage_id %]&item_id=[% item.bib_item.id %]&biblionumber=[% biblionumber %]"> >+ [% ELSE %] >+ <a class="btn btn-default btn-xs" disabled> >+ [% END %] >+ <i class="fa fa-fire"></i> >+ [% item.stockrotationitem.indemand ? 'Remove "In demand"' : 'Add "In demand"' %] >+ </a> >+ [% IF !in_transit %] >+ <a class="btn btn-default btn-xs" href="?op=confirm_remove_from_rota&stage_id=[% item.stockrotationitem.stage.stage_id %]&item_id=[% item.bib_item.id %]&biblionumber=[% biblionumber %]"> >+ [% ELSE %] >+ <a class="btn btn-default btn-xs" disabled> >+ [% END %] >+ <i class="fa fa-trash"></i> >+ Remove from rota >+ </a> >+ [% ELSE %] >+ <form class="rota_select_form" method="post" enctype="multipart/form-data"> >+ <select class="item_select_rota" name="rota_id"> >+ [% FOREACH rota IN rotas %] >+ <option value="[% rota.rota_id %]">[% rota.title %]</option> >+ [% END %] >+ </select> >+ <button class="btn btn-default btn-xs" type="submit"><i class="fa fa-plus"></i> Add to rota</button> >+ <input type="hidden" name="op" value="add_item_to_rota"></input> >+ <input type="hidden" name="item_id" value="[% item.bib_item.id %]"></input> >+ <input type="hidden" name="biblionumber" value="[% biblionumber %]"></input> >+ </form> >+ [% END %] >+ </td> >+ </tr> >+ [% END %] >+ </tbody> >+ </table> >+ [% END %] >+ [% IF !items || items.size == 0 %] >+ <h1>No physical items for this record</h1> >+ [% END %] >+ [% IF !rotas || rotas.count == 0 %] >+ <h1>There are no rotas with stages assigned</h1> >+ [% END %] >+ [% ELSIF op == 'confirm_remove_from_rota' %] >+ <div class="dialog alert"> >+ <h3>Are you sure you want to remove this item from it's rota?</h3> >+ <p> >+ <a class="btn btn-default btn-xs approve" href="?op=remove_item_from_stage&stage_id=[% stage_id %]&item_id=[% item_id %]&biblionumber=[% biblionumber %]"><i class="fa fa-fw fa-check"></i>Yes</a> >+ <a class="btn btn-default btn-xs deny" href="?biblionumber=[% biblionumber %]"><i class="fa fa-fw fa-remove"></i>No</a> >+ </p> >+ </div> >+ [% END %] >+ >+</div> >+ >+</div> >+</div> >+<div class="yui-b"> >+[% INCLUDE 'biblio-view-menu.inc' %] >+</div> >+</div> >+[% INCLUDE 'intranet-bottom.inc' %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stockrotation.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stockrotation.tt >new file mode 100644 >index 0000000000..e9cf827fad >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stockrotation.tt >@@ -0,0 +1,482 @@ >+[% USE Koha %] >+[% USE Branches %] >+[% USE KohaDates %] >+[% INCLUDE 'doc-head-open.inc' %] >+<title>Koha › Stock rotation</title> >+[% INCLUDE 'doc-head-close.inc' %] >+<link rel="stylesheet" type="text/css" href="[% interface %]/[% theme %]/css/datatables.css" /> >+[% INCLUDE 'datatables.inc' %] >+<script type="text/javascript" src="[% interface %]/[% theme %]/js/pages/stockrotation.js"></script> >+<script type="text/javascript"> >+//<![CDATA[ >+ $(document).ready(function() { >+ $('#sr_manage_items').dataTable($.extend(true, {}, dataTablesDefaults, { >+ "autoWidth": false, >+ "aoColumnDefs": [ >+ { "bSortable": false, "bSearchable": false, 'aTargets': [ 'NoSort' ] }, >+ { "bSortable": true, "bSearchable": false, 'aTargets': [ 'NoSearch' ] } >+ ], >+ "sPaginationType": "four_button" >+ })); >+ }); >+//]]> >+</script> >+</head> >+ >+<body> >+[% INCLUDE 'header.inc' %] >+[% INCLUDE 'patron-search.inc' %] >+ >+<div id="breadcrumbs"> >+ <a href="/cgi-bin/koha/mainpage.pl">Home</a> >+ › <a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a> >+ >+[% IF no_op_set %] >+ › Stock rotation >+[% ELSE %] >+ › <a href="/cgi-bin/koha/tools/stockrotation.pl">Stock rotation</a> >+[% END %] >+ >+[% IF (op == 'create_edit_rota' && rota.rota_id) %] >+ › Edit rota >+[% ELSIF (op == 'create_edit_rota' && !rota.rota_id) %] >+ › Create rota >+[% ELSIF (op == 'manage_stages') %] >+ › Manage stages >+[% ELSIF (op == 'create_edit_stage' && stage.id) %] >+ <a href="?op=manage_stages&rota_id=[% rota_id %]">› Manage stages</a> >+ › Edit stage >+[% ELSIF (op == 'create_edit_stage' && !stage.id) %] >+ <a href="?op=manage_stages&rota_id=[% rota_id %]">› Manage stages</a> >+ › Create stage >+[% ELSIF (op == 'manage_items') %] >+ › Manage items >+[% END %] >+ >+</div> >+ >+<div id="doc3" class="yui-t2"> >+ <div id="bd"> >+ <div id="yui-main"> >+ <div id="stockrotation" class="yui-b"> >+ >+ <h1>Stock rotation</h1> >+ >+ [% IF no_op_set %] >+ >+ [% INCLUDE 'stockrotation-toolbar.inc' %] >+ >+ [% IF existing_rotas.size > 0 %] >+ <h3>Manage existing rotas</h3> >+ <table class="rotas_table" role="grid"> >+ <thead> >+ <th>Name</th> >+ <th>Cyclical</th> >+ <th>Active</th> >+ <th>Description</th> >+ <th>Number of items</th> >+ <th> </th> >+ </thead> >+ <tbody> >+ [% FOREACH rota IN existing_rotas %] >+ <tr> >+ <td>[% rota.title %]</td> >+ <td>[% rota.cyclical ? 'Yes' : 'No'%]</td> >+ <td>[% rota.active ? 'Yes' : 'No'%]</td> >+ <td>[% rota.description %]</td> >+ <td>[% rota.stockrotationitems.count %]</td> >+ <td class="actions"> >+ <a class="btn btn-default btn-xs" href="?op=create_edit_rota&rota_id=[% rota.rota_id %]"> >+ <i class="fa fa-pencil"></i> >+ Edit >+ </a> >+ <div class="btn-group" role="group"> >+ <button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> >+ Manage >+ <i class="fa fa-caret-down"></i> >+ </button> >+ <ul class="dropdown-menu"> >+ <li><a href="?op=manage_stages&rota_id=[% rota.rota_id %]">Stages</a></li> >+ [% IF CAN_user_stockrotation_can_add_items_rotas && rota.stockrotationstages.count > 0 %] >+ <li><a href="?op=manage_items&rota_id=[% rota.rota_id %]">Items</a></li> >+ [% END %] >+ </ul> >+ </div> >+ <a class="btn btn-default btn-xs" href="?op=toggle_rota&rota_id=[% rota.rota_id %]"> >+ <i class="fa fa-power-off"></i> >+ [% IF !rota.active %] >+ Activate >+ [% ELSE %] >+ Deactivate >+ [% END %] >+ </a> >+ </td> >+ </tr> >+ [% END %] >+ </tbody> >+ </table> >+ [% END %] >+ >+ [% ELSIF (op == 'create_edit_rota') %] >+ >+ [% IF rota.rota_id %] >+ <h3>Edit "[% rota.title %]"</h3> >+ [% ELSE %] >+ <h3>Create new rota</h3> >+ [% END %] >+ >+ [% IF error == 'invalid_form' %] >+ <div class="dialog alert"> >+ <h3>There was a problem with your form submission</h3> >+ </div> >+ [% END %] >+ >+ <form id="rota_form" method="post" enctype="multipart/form-data" class="validated"> >+ <fieldset class="rows"> >+ <ol> >+ <li> >+ <label class="required" for="title">Name:</label> >+ <input type="text" id="title" name="title" value="[% rota.title %]" required="required" placeholder="Rota name"> >+ <span class="required">Required</span> >+ </li> >+ <li> >+ <label for="cyclical">Cyclical:</label> >+ <select name="cyclical" id="cyclical"> >+ [% IF rota.cyclical %] >+ <option value="1" selected>Yes</option> >+ <option value="0">No</option> >+ [% ELSE %] >+ <option value="1">Yes</option> >+ <option value="0" selected>No</option> >+ [% END %] >+ </select> >+ </li> >+ <li> >+ <label for="active">Active:</label> >+ <select name="active" id="active"> >+ [% IF rota.active %] >+ <option value="1" selected>Yes</option> >+ <option value="0">No</option> >+ [% ELSE %] >+ <option value="1">Yes</option> >+ <option value="0" selected>No</option> >+ [% END %] >+ </select> >+ </li> >+ <li> >+ <label for="description">Description:</label> >+ <textarea id="description" name="description" placeholder="Rota description">[% rota.description %]</textarea> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <input type="submit" value="Save"> >+ <a href="/cgi-bin/koha/tools/stockrotation.pl" class="cancel">Cancel</a> >+ </fieldset> >+ [% IF rota.rota_id %] >+ <input type="hidden" name="id" value="[% rota.rota_id %]"> >+ [% END %] >+ <input type="hidden" name="op" value="process_rota"> >+ </form> >+ >+ [% ELSIF (op == 'manage_stages') %] >+ >+ [% INCLUDE 'stockrotation-toolbar.inc' %] >+ >+ [% IF existing_stages.size > 0 %] >+ <div id="manage_stages"> >+ <h3>Manage stages</h3> >+ <div id="ajax_status" >+ data-saving-msg="Saving changes..." >+ data-success-msg="" >+ data-failed-msg="Error: "> >+ <span id="ajax_saving_msg"></span> >+ <i id="ajax_saving_icon" class="fa fa-spinner fa-spin"></i> >+ <i id="ajax_success_icon" class="fa fa-check"></i> >+ <i id="ajax_failed_icon" class="fa fa-times"></i> >+ <span id="ajax_success_msg"></span> >+ <span id="ajax_failed_msg"></span> >+ </div> >+ <div id="manage_stages_help"> >+ 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 >+ </div> >+ <div id="stage_list_headings"> >+ <span class="stagename">Branch</span> >+ <span class="stageduration">Duration (days)</span> >+ </div> >+ <ul id="sortable_stages" data-rota-id="[% rota.rota_id %]"> >+ [% FOREACH stage IN existing_stages %] >+ <li id="stage_[% stage.stage_id %]"> >+ <span data-toggle="tooltip" title="Drag and drop to move this stage to another position" data-placement="right" class="stagename"> >+ [% IF existing_stages.size > 1 %] >+ <i class="drag_handle fa fa-lg fa-bars"></i> >+ [% END %] >+ [% Branches.GetName(stage.branchcode_id) %] >+ </span> >+ <span class="stageduration">[% stage.duration %]</span> >+ <span class="stageactions"> >+ <a class="btn btn-default btn-xs" href="?op=create_edit_stage&stage_id=[% stage.stage_id %]"> >+ <i class="fa fa-pencil"></i> Edit >+ </a> >+ <a class="btn btn-default btn-xs" href="?op=confirm_delete_stage&stage_id=[% stage.stage_id %]"> >+ <i class="fa fa-trash"></i> Delete >+ </a> >+ </span> >+ </li> >+ [% END %] >+ </ul> >+ </div> >+ [% END %] >+ >+ <p><a href="stockrotation.pl">Return to rotas</a></p> >+ >+ [% ELSIF (op == 'create_edit_stage') %] >+ >+ [% IF stage.id %] >+ <h3>Edit "[% Branches.GetName(stage.branchcode_id) %]"</h3> >+ [% ELSE %] >+ <h3>Create new stage</h3> >+ [% END %] >+ >+ [% IF error == 'invalid_form' %] >+ <div class="dialog alert"> >+ <h3>There was a problem with your form submission</h3> >+ </div> >+ [% END %] >+ >+ <form id="stage_form" method="post" enctype="multipart/form-data" class="validated"> >+ <fieldset class="rows"> >+ <ol> >+ <li> >+ <label class="required" for="branch">Branch:</label> >+ <select name="branchcode" id="branch"> >+ [% FOREACH branch IN branches %] >+ [% IF branch.branchcode == stage.branchcode_id %] >+ <option value="[% branch.branchcode %]" selected>[% Branches.GetName(branch.branchcode) %]</option> >+ [% ELSE %] >+ <option value="[% branch.branchcode %]">[% Branches.GetName(branch.branchcode) %]</option> >+ [% END %] >+ [% END %] >+ </select> >+ <span class="required">Required</span> >+ </li> >+ <li> >+ <label class="required" for="duration">Duration:</label> >+ <input type="text" id="duration" name="duration" value="[% stage.duration %]" required="required" placeholder="Duration (days)"> >+ <span class="required">Required</span> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <input type="submit" value="Save"> >+ <a href="/cgi-bin/koha/tools/stockrotation.pl?op=manage_stages&rota_id=[% rota_id %]" class="cancel">Cancel</a> >+ </fieldset> >+ <input type="hidden" name="stage_id" value="[% stage.id %]"> >+ <input type="hidden" name="rota_id" value="[% rota_id %]"> >+ <input type="hidden" name="op" value="process_stage"> >+ </form> >+ [% ELSIF (op == 'confirm_remove_from_rota') %] >+ >+ <div class="dialog alert"> >+ <h3>Are you sure you wish to remove this item from it's rota</h3> >+ <p> >+ <a class="btn btn-default btn-xs approve" href="?op=remove_item_from_stage&item_id=[% item_id %]&stage_id=[% stage_id %]&rota_id=[% rota_id %]"><i class="fa fa-fw fa-check"></i>Yes</a> >+ <a class="btn btn-default btn-xs deny" href="?op=manage_items&rota_id=[% rota_id %]"><i class="fa fa-fw fa-remove"></i>No</a> >+ </p> >+ </div> >+ [% ELSIF (op == 'confirm_delete_stage') %] >+ >+ <div class="dialog alert"> >+ <h3>Are you sure you want to delete this stage?</h3> >+ [% IF stage.stockrotationitems.count > 0 %] >+ <p>This stage contains the following item(s):</p> >+ <ul> >+ [% FOREACH item IN stage.stockrotationitems %] >+ <li>[% item.itemnumber.biblio.title %] (Barcode: [% item.itemnumber.barcode %])</li> >+ [% END %] >+ </ul> >+ [% END %] >+ <p> >+ <a class="btn btn-default btn-xs approve" href="?op=delete_stage&stage_id=[% stage.stage_id %]"><i class="fa fa-fw fa-check"></i>Yes</a> >+ <a class="btn btn-default btn-xs deny" href="?op=manage_stages&rota_id=[% stage.rota.rota_id %]"><i class="fa fa-fw fa-remove"></i>No</a> >+ </p> >+ </div> >+ [% ELSIF (op == 'manage_items') %] >+ >+ [% INCLUDE 'stockrotation-toolbar.inc' %] >+ >+ [% IF error %] >+ <div class="dialog alert"> >+ [% IF error == "item_not_found" %] >+ <h3>The item was not found</h3> >+ [% ELSIF error == "already_on_rota" %] >+ <h3>This item is already on this rota</h3> >+ [% END %] >+ </div> >+ [% END %] >+ >+ <div> >+ <form id="add_rota_item_form" method="post" enctype="multipart/form-data" class="validated"> >+ <fieldset class="rows"> >+ <legend>Add item to "[% rota.title %]"</legend> >+ <ol> >+ <li> >+ <label for="barcode">Barcode:</label> >+ <input type="text" id="barcode" name="barcode" placeholder="Item barcode" autofocus> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="rows"> >+ <legend>Use a barcode file</legend> >+ <ol> >+ <li> >+ <label for="barcodefile">Barcode file:</label> >+ <input type="file" id="barcodefile" name="barcodefile"> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <input type="submit" value="Save"> >+ </fieldset> >+ <input type="hidden" name="rota_id" value="[% rota.id %]"> >+ <input type="hidden" name="op" value="add_items_to_rota"> >+ </form> >+ </div> >+ >+ [% IF items.count > 0 %] >+ <h3>Manage items assigned to "[% rota.title %]"</h3> >+ <table id="sr_manage_items" class="items_table" role="grid"> >+ <thead> >+ <th>Barcode</th> >+ <th>Title</th> >+ <th>Author</th> >+ <th>Shelfmark</th> >+ <th class="NoSearch">In transit</th> >+ <th class="NoSort">Stages & duration in days<br>(current stage highlighted)</th> >+ <th class="NoSort"> </th> >+ </thead> >+ <tbody> >+ [% FOREACH item IN items %] >+ <tr> >+ <td><a href="/cgi-bin/koha/catalogue/moredetail.pl?itemnumber=[% item.id %]&biblionumber=[% item.itemnumber.biblio.id %]#item[% item.id %]">[% item.itemnumber.barcode %]</a></td> >+ <td><a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% item.itemnumber.biblio.id %]">[% item.itemnumber.biblio.title %]</a></td> >+ <td>[% item.itemnumber.biblio.author %]</td> >+ <td>[% item.itemnumber.itemcallnumber %]</td> >+ <td>[% item.itemnumber.get_transfer ? 'Yes' : 'No' %]</td> >+ <td> >+ [% FOREACH this_stage IN stages %] >+ [% IF this_stage.stage_id == item.stage.stage_id %] >+ <span class="stage highlight_stage"> >+ [% ELSE %] >+ <span class="stage"> >+ [% END %] >+ [% Branches.GetName(this_stage.branchcode_id) %] ([% this_stage.duration %]) >+ </span> >+ » >+ [% END %] >+ [% IF stages.size > 0 %] >+ <span class="stage">[% rota.cyclical ? 'START' : 'END' %]</span> >+ [% END %] >+ </td> >+ <td class="actions"> >+ [% in_transit = item.itemnumber.get_transfer %] >+ [% IF !in_transit && stages.size > 1 %] >+ <a class="btn btn-default btn-xs" href="?op=move_to_next_stage&rota_id=[% rota.id %]&item_id=[% item.id %]&stage_id=[% item.stage.stage_id %]"> >+ [% ELSE %] >+ <a class="btn btn-default btn-xs" disabled> >+ [% END %] >+ <i class="fa fa-arrow-right"></i> >+ Move to next stage >+ </a> >+ [% IF !in_transit %] >+ <a class="btn btn-default btn-xs" href="?op=toggle_in_demand&stage_id=[% item.stage.stage_id %]&item_id=[% item.id %]&rota_id=[% rota.id %]"> >+ [% ELSE %] >+ <a class="btn btn-default btn-xs" disabled> >+ [% END %] >+ <i class="fa fa-fire"></i> >+ [% item.indemand ? 'Remove "In demand"' : 'Add "In demand"' %] >+ </a> >+ [% IF !in_transit %] >+ <a class="btn btn-default btn-xs" href="?op=confirm_remove_from_rota&stage_id=[% item.stage.stage_id %]&item_id=[% item.id %]&rota_id=[% rota.id %]"> >+ [% ELSE %] >+ <a class="btn btn-default btn-xs" disabled> >+ [% END %] >+ <i class="fa fa-trash"></i> >+ Remove from rota >+ </a> >+ </td> >+ </tr> >+ [% END %] >+ </tbody> >+ </table> >+ [% END %] >+ >+ <p><a href="stockrotation.pl">Return to rotas</a></p> >+ >+ [% ELSIF op == 'add_items_to_rota' %] >+ >+ >+ <div class="dialog message"> >+ <h3>Add items to rota report</h3> >+ </div> >+ <div> >+ [% IF barcode_status.ok.size > 0 %] >+ <h4>Items added to rota:</h4> >+ <ul> >+ [% FOREACH item_ok IN barcode_status.ok %] >+ <li>[% item_ok.biblio.title %]</li> >+ [% END %] >+ </ul> >+ [% END %] >+ [% IF barcode_status.on_this.size > 0 %] >+ <h4>Items already on this rota:</h4> >+ <ul> >+ [% FOREACH item_on_this IN barcode_status.on_this %] >+ <li>[% item_on_this.biblio.title %]</li> >+ [% END %] >+ </ul> >+ [% END %] >+ [% IF barcode_status.not_found.size > 0 %] >+ <h4>Barcodes not found:</h4> >+ <ul> >+ [% FOREACH barcode_not_found IN barcode_status.not_found %] >+ <li>[% barcode_not_found %]</li> >+ [% END %] >+ </ul> >+ [% END %] >+ [% IF barcode_status.on_other.size > 0 %] >+ <h4>Items found on other rotas:</h4> >+ <ul> >+ [% FOREACH item_on_other IN barcode_status.on_other %] >+ <li>[% item_on_other.biblio.title %]</li> >+ [% END %] >+ </ul> >+ [% END %] >+ </div> >+ [% IF barcode_status.on_other.size > 0 %] >+ <form id="add_rota_item_form" method="post" enctype="multipart/form-data"> >+ <fieldset> >+ <legend>Select items to move to this rota:</legend> >+ [% FOREACH item_on_other IN barcode_status.on_other %] >+ <li><input type="checkbox" name="move_item" value="[% item_on_other.itemnumber %]"> [% item_on_other.biblio.title %] (Currently on "[% item_on_other.stockrotationitem.stage.rota.title %]")</li> >+ [% END %] >+ >+ </fieldset> >+ <fieldset class="action"> >+ <input type="submit" value="Save"> >+ </fieldset> >+ <input type="hidden" name="rota_id" value="[% rota_id %]"> >+ <input type="hidden" name="op" value="move_items_to_rota"> >+ </form> >+ [% END %] >+ <p><a href="?op=manage_items&rota_id=[% rota_id %]">Return to rota</a></p> >+ >+ [% END %] >+ </div> >+ </div> >+ <div class="yui-b"> >+ [% INCLUDE 'tools-menu.inc' %] >+ </div> >+ </div> >+</div> >+[% INCLUDE 'intranet-bottom.inc' %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt >index 4c8cca9192..0ff70a27dd 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt >@@ -1,3 +1,5 @@ >+[% USE Koha %] >+ > [% INCLUDE 'doc-head-open.inc' %] > <title>Koha › Tools</title> > [% INCLUDE 'doc-head-close.inc' %] >@@ -69,6 +71,11 @@ > <dt><a href="/cgi-bin/koha/tools/picture-upload.pl">Upload patron images</a></dt> > <dd>Upload patron images in a batch or one at a time</dd> > [% END %] >+ >+ [% IF ( CAN_user_stockrotation_can_edit_rotas && Koha.Preference('StockRotation') ) %] >+ <dt><a href="/cgi-bin/koha/tools/stockrotation.pl">Stock rotation</a></dt> >+ <dd>Manage Stock rotation rotas, rota stages and rota items</dd> >+ [% END %] > </dl> > </div> > <div class="yui-u"> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/pages/stockrotation.js b/koha-tmpl/intranet-tmpl/prog/js/pages/stockrotation.js >new file mode 100644 >index 0000000000..276394518b >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/pages/stockrotation.js >@@ -0,0 +1,65 @@ >+function init() { >+ $('#ajax_status').hide(); >+ $('#ajax_saving_msg').hide(); >+ $('#ajax_saving_icon').hide(); >+ $('#ajax_success_icon').hide(); >+ $('#ajax_failed_icon').hide(); >+ $('#ajax_failed_msg').hide(); >+} >+ >+$(document).ready(function() { >+ var apiEndpoint = '/api/v1/rotas/'; >+ init(); >+ $('#sortable_stages').sortable({ >+ handle: '.drag_handle', >+ placeholder: 'drag_placeholder', >+ update: function(event, ui) { >+ init(); >+ $('#sortable_stages').sortable('disable'); >+ var rotaId = document.getElementById('sortable_stages').dataset.rotaId; >+ $('#ajax_saving_msg').text( >+ document.getElementById('ajax_status').dataset.savingMsg >+ ); >+ $('#ajax_saving_icon').show(); >+ $('#ajax_saving_msg').show(); >+ $('#ajax_status').fadeIn(); >+ var stageId = ui.item[0].id.replace(/^stage_/, ''); >+ var newIndex = ui.item.index(); >+ var newPosition = newIndex + 1; >+ $.ajax({ >+ method: 'PUT', >+ url: apiEndpoint + rotaId + '/stages/' + stageId + '/position', >+ processData: false, >+ contentType: 'application/json', >+ data: newPosition >+ }) >+ .done(function(data) { >+ $('#ajax_success_msg').text( >+ document.getElementById('ajax_status').dataset.successMsg >+ ); >+ $('#ajax_saving_icon').hide(); >+ $('#ajax_success_icon').show(); >+ $('#ajax_success_msg').show(); >+ setTimeout( >+ function() { >+ $('#ajax_status').fadeOut(); >+ }, >+ 700 >+ ); >+ }) >+ .fail(function(jqXHR, status, error) { >+ $('#ajax_failed_msg').text( >+ document.getElementById('ajax_status').dataset.failedMsg + >+ error >+ ); >+ $('#ajax_saving_icon').hide(); >+ $('#ajax_failed_icon').show(); >+ $('#ajax_failed_msg').show(); >+ $('#sortable_stages').sortable('cancel'); >+ }) >+ .always(function() { >+ $('#sortable_stages').sortable('enable'); >+ }) >+ } >+ }); >+}); >diff --git a/misc/cronjobs/stockrotation.pl b/misc/cronjobs/stockrotation.pl >new file mode 100755 >index 0000000000..d8914e42d9 >--- /dev/null >+++ b/misc/cronjobs/stockrotation.pl >@@ -0,0 +1,489 @@ >+#!/usr/bin/perl >+ >+# Copyright 2016 PTFS Europe >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+=head1 NAME >+ >+stockrotation.pl >+ >+=head1 SYNOPSIS >+ >+ --[a]dmin-email An address to which email reports should also be sent >+ --[b]ranchcode Select branch to report on for 'email' reports (default: all) >+ --e[x]ecute Actually perform stockrotation housekeeping >+ --[r]eport Select either 'full' or 'email' >+ --[S]end-all Send email reports even if the report body is empty >+ --[s]end-email Send reports by email >+ --[h]elp Display this help message >+ >+Cron script implementing scheduled stockrotation functionality. >+ >+By default this script merely reports on the current status of the >+stockrotation subsystem. In order to actually place items in transit, the >+script must be run with the `execute` argument. >+ >+`report` allows you to select the type of report that will be emitted. It's >+set to 'full' by default. If the `email` report is selected, you can use the >+`branchcode` parameter to specify which branch's report you would like to see. >+The default is 'all'. >+ >+`admin-email` is an additional email address to which we will send all email >+reports in addition to sending them to branch email addresses. >+ >+`send-email` will cause the script to send reports by email, and `send-all` >+will cause even reports with an empty body to be sent. >+ >+=head1 DESCRIPTION >+ >+This script is used to move items from one stockrotationstage to the next, >+if they are elible for processing. >+ >+it should be run from cron like: >+ >+ stockrotation.pl --report email --send-email --execute >+ >+Prior to that you can run the script from the command line without the >+--execute and --send-email parameters to see what reports the script would >+generate in 'production' mode. This is immensely useful for testing, or for >+getting to understand how the stockrotation module works: you can set up >+different scenarios, and then "query" the system on what it would do. >+ >+Normally you would want to run this script once per day, probably around >+midnight-ish to move any stockrotationitems along their rotas and to generate >+the email reports for branch libraries. >+ >+Each library will receive a report with "items of interest" for them for >+today's rota checks. Each item there will be an item that should, according >+to Koha, be located on the shelves of that branch, and which should be picked >+up and checked in. The item will either: >+- have been placed in transit to their new stage library; >+- have been placed in transit to be returned to their current stage library; >+- have just been added to a rota and will already be at the correct library; >+ >+In the last case the item will be checked in and no message will pop up. In >+the other cases a message will pop up requesting the item be posted to their >+new branch. >+ >+=head2 What does the --execute flag do? >+ >+To understand this, you will need to know a little bit about the design of >+this script and the stockrotation modules. >+ >+This script operates in 3 phases: first it walks the graph of rotas, stages >+and items. For each active rota, it investigates the items in each stage and >+determines whether action is required. It does not perform any actions, it >+just "sieves" all items on active rotas into "actionable" and "non-actionable" >+baskets. We can use these baskets to perform actions against the items, or to >+generate reports. >+ >+During the second phase this script then loops through the actionable baskets, >+and performs the relevant action (initiate, repatriate, advance) on each item. >+ >+Finally, during the third phase we revisit the original baskets and we compile >+reports (for instance per branch email reports). >+ >+When the script is run without the "--execute" flag, we perform phase 1, skip >+phase 2 and move straight onto phase 3. >+ >+With the "--execute" flag we also perform the database operations. >+ >+So with or without the flag, the report will look the same (except for the "No >+database updates have been performed."). >+ >+=cut >+ >+use Modern::Perl; >+use Getopt::Long qw/HelpMessage :config gnu_getopt/; >+use Mail::Sendmail; >+use C4::Context; >+use Koha::Email; >+use Koha::StockRotationRotas; >+ >+my $admin_email = ''; >+my $branch = 0; >+my $execute = 0; >+my $report = 'full'; >+my $send_all = 0; >+my $send_email = 0; >+ >+my $ok = GetOptions( >+ 'admin-email|a=s' => \$admin_email, >+ 'branchcode|b=s' => sub { >+ my ($opt_name, $opt_value) = @_; >+ my $branches = Koha::Libraries->search( >+ {}, { order_by => { -asc => 'branchname'} } >+ ); >+ my $brnch = $branches->find($opt_value); >+ if ( $brnch ) { >+ $branch = $brnch; >+ return $brnch; >+ } else { >+ printf("Option $opt_name should be one of (name -> code):\n"); >+ while ( my $candidate = $branches->next ) { >+ printf( >+ " %-40s -> %s\n", $candidate->branchname, >+ $candidate->branchcode >+ ); >+ } >+ exit 1 >+ } >+ }, >+ 'execute|x' => \$execute, >+ 'report|r=s' => sub { >+ my ($opt_name, $opt_value) = @_; >+ if ( $opt_value eq 'full' || $opt_value eq 'email' ) { >+ $report = $opt_value; >+ } else { >+ printf("Option $opt_name should be either 'email' or 'full'.\n"); >+ exit 1; >+ } >+ }, >+ 'send-all|S' => \$send_all, >+ 'send-email|s' => \$send_email, >+ 'help|h|?' => sub { HelpMessage } >+); >+exit 1 unless ( $ok ); >+ >+$send_email++ if ( $send_all ); # if we send all, then we must want emails. >+ >+=head2 Helpers >+ >+=head3 execute >+ >+ undef = execute($report); >+ >+Perform the database updates, within a transaction, that are reported as >+needing to be performed by $REPORT. >+ >+$REPORT should be the return value of an invocation of `investigate`. >+ >+This procedure WILL mess with your database. >+ >+=cut >+ >+sub execute { >+ my ( $data ) = @_; >+ >+ # Begin transaction >+ my $schema = Koha::Database->new->schema; >+ $schema->storage->txn_begin; >+ >+ # Carry out db updates >+ foreach my $item (@{$data->{items}}) { >+ my $reason = $item->{reason}; >+ if ( $reason eq 'repatriation' ) { >+ $item->{object}->repatriate; >+ } elsif ( grep { $reason eq $_ } >+ qw/in-demand advancement initiation/ ) { >+ $item->{object}->advance; >+ } >+ } >+ >+ # End transaction >+ $schema->storage->txn_commit; >+} >+ >+=head3 report_full >+ >+ my $full_report = report_full($report); >+ >+Return an arrayref containing a string containing a detailed report about the >+current state of the stockrotation subsystem. >+ >+$REPORT should be the return value of `investigate`. >+ >+No data in the database is manipulated by this procedure. >+ >+=cut >+ >+sub report_full { >+ my ( $data ) = @_; >+ >+ my $header = ""; >+ my $body = ""; >+ # Summary >+ $header .= sprintf " >+STOCKROTATION REPORT >+-------------------- >+ Total number of rotas: %5u >+ Inactive rotas: %5u >+ Active rotas: %5u >+ Total number of items: %5u >+ Inactive items: %5u >+ Stationary items: %5u >+ Actionable items: %5u >+ Total items to be initiated: %5u >+ Total items to be repatriated: %5u >+ Total items to be advanced: %5u >+ Total items in demand: %5u\n\n", >+ $data->{sum_rotas}, $data->{rotas_inactive}, $data->{rotas_active}, >+ $data->{sum_items}, $data->{items_inactive}, $data->{stationary}, >+ $data->{actionable}, $data->{initiable}, $data->{repatriable}, >+ $data->{advanceable}, $data->{indemand}; >+ >+ if (@{$data->{rotas}}) { # Per Rota details >+ $body .= sprintf "ROTAS DETAIL\n------------\n\n"; >+ foreach my $rota (@{$data->{rotas}}) { >+ $body .= sprintf "Details for %s [%s]:\n", >+ $rota->{name}, $rota->{id}; >+ $body .= sprintf "\n Items:"; # Rota item details >+ if (@{$rota->{items}}) { >+ $body .= join("", map { _print_item($_) } @{$rota->{items}}); >+ } else { >+ $body .= sprintf >+ "\n No items to be processed for this rota.\n"; >+ } >+ $body .= sprintf "\n Log:"; # Rota log details >+ if (@{$rota->{log}}) { >+ $body .= join("", map { _print_item($_) } @{$rota->{log}}); >+ } else { >+ $body .= sprintf "\n No items in log for this rota.\n\n"; >+ } >+ } >+ } >+ return [ $header, { >+ body => $body, # The body of the report >+ status => 1, # We have a meaningful report >+ no_branch_email => 1, # We don't expect branch email in report >+ } ]; >+} >+ >+=head3 report_email >+ >+ my $email_report = report_email($report); >+ >+Returns an arrayref containing a header string, with basic report information, >+and any number of 'per_branch' strings, containing a detailed report about the >+current state of the stockrotation subsystem, from the perspective of those >+individual branches. >+ >+$REPORT should be the return value of `investigate`, and $BRANCH should be >+either 0 (to indicate 'all'), or a specific Koha::Library object. >+ >+No data in the database is manipulated by this procedure. >+ >+=cut >+ >+sub report_email { >+ my ( $data, $branch ) = @_; >+ >+ my $out = []; >+ my $header = ""; >+ # Summary >+ my $branched = $data->{branched}; >+ my $flag = 0; >+ >+ $header .= sprintf " >+BRANCH-BASED STOCKROTATION REPORT >+---------------------------------\n"; >+ push @{$out}, $header; >+ >+ if ( $branch ) { # Branch limited report >+ push @{$out}, _report_per_branch( >+ $branched->{$branch->branchcode}, $branch->branchcode, >+ $branch->branchname >+ ); >+ } elsif ( $data->{actionable} ) { # Full email report >+ while ( my ($branchcode_id, $details) = each %{$branched} ) { >+ push @{$out}, _report_per_branch( >+ $details, $details->{code}, $details->{name} >+ ) if ( @{$details->{items}} ); >+ } >+ } else { >+ push @{$out}, { >+ body => sprintf " >+No actionable items at any libraries.\n\n", # The body of the report >+ no_branch_email => 1, # We don't expect branch email in report >+ }; >+ } >+ return $out; >+} >+ >+=head3 _report_per_branch >+ >+ my $branch_string = _report_per_branch($branch_details, $branchcode, $branchname); >+ >+return a string containing details about the stockrotation items and their >+status for the branch identified by $BRANCHCODE. >+ >+This helper procedure is only used from within `report_email`. >+ >+No data in the database is manipulated by this procedure. >+ >+=cut >+ >+sub _report_per_branch { >+ my ( $per_branch, $branchcode, $branchname ) = @_; >+ >+ my $out = ""; >+ my $status = 0; >+ >+ $out .= sprintf "\nStockrotation report for '%s' [%s]:\n", >+ $branchname || "N/A", $branchcode || "N/A"; >+ if ( $per_branch && @{$per_branch->{items}} ) { >+ $out .= sprintf " >+ Email: %s >+ Phone: %s >+ Items:", >+ $per_branch->{email} || "N/A", $per_branch->{phone} || "N/A"; >+ $status++; >+ } else { >+ $out .= sprintf "No items to be processed for this branch.\n\n"; >+ } >+ $out .= join("", map { >+ _print_item($_) unless $_->{reason} eq 'in-demand' >+ } @{$per_branch->{items}}); >+ return { >+ body => $out, # The body of the report >+ email_address => $per_branch->{email}, # The branch email address >+ status => $status, # We may have empty report... >+ }; >+} >+ >+=head3 _print_item >+ >+ my $string = _print_item($item_section); >+ >+Return a string containing an overview about $ITEM_SECTION. >+ >+This helper procedure is only used from within `report_email` and >+`report_full`. >+ >+No data in the database is manipulated by this procedure. >+ >+=cut >+ >+sub _print_item { >+ my ( $item ) = @_; >+ return sprintf " >+ Title: %s >+ Author: %s >+ Callnumber: %s >+ Location: %s >+ Barcode: %s >+ On loan?: %s >+ Status: %s >+ Current Library: %s [%s]\n\n", >+ $item->{title} || "N/A", $item->{author} || "N/A", >+ $item->{callnumber} || "N/A", $item->{location} || "N/A", >+ $item->{barcode} || "N/A", $item->{onloan} ? 'Yes' : 'No', >+ $item->{reason} || "N/A", $item->{branch}->branchname, >+ $item->{branch}->branchcode; >+} >+ >+=head3 emit >+ >+ undef = emit($params); >+ >+$PARAMS should be a hashref of the following format: >+ admin_email: the address to which a copy of all reports should be sent. >+ execute: the flag indicating whether we performed db updates >+ send_all: the flag indicating whether we should send even empty reports >+ send_email: the flag indicating whether we want to emit to stdout or email >+ report: the data structure returned from one of the report procedures >+ >+No data in the database is manipulated by this procedure. >+ >+The return value is unspecified: we simply emit a message as a side-effect or >+die. >+ >+=cut >+ >+sub emit { >+ my ( $params ) = @_; >+ >+ # REPORT is an arrayref of at least 2 elements: >+ # - The header for the report, which will be repeated for each part >+ # - a "part" for each report we want to emit >+ # PARTS are hashrefs: >+ # - part->{body}: the body of the report >+ # - part->{email_address}: the email address to send the report to >+ my $report = $params->{report}; >+ my $header = shift @{$report}; >+ my $parts = $report; >+ >+ foreach my $part (@{$parts}) { >+ # The final message is the header + body of this part. >+ my $msg = $header . $part->{body}; >+ $msg .= "No database updates have been performed.\n\n" >+ unless ( $params->{execute} ); >+ >+ if ( $params->{send_email} ) { # Only email if emails requested >+ if ( $part->{status} || $params->{send_all} ) { >+ # We have a report to send, or we want to send even empty >+ # reports. >+ my $message = Koha::Email->new; >+ >+ # Collate 'to' addresses >+ my @to = (); >+ if ( $part->{email_address} ) { >+ push @to, $part->{email_address}; >+ } elsif ( !$part->{no_branch_email} ) { >+ $msg = "***We tried to send a branch report, but we have no email address for this branch.***\n\n" . $msg; >+ push @to, C4::Context->preference('KohaAdminEmailAddress') >+ if ( C4::Context->preference('KohaAdminEmailAddress') ); >+ } >+ push @to, $params->{admin_email} if $params->{admin_email}; >+ >+ # Create email data. >+ my %mail = $message->create_message_headers( >+ { >+ to => join("; ", @to), >+ subject => "Stockrotation Email Report", >+ message => Encode::encode("utf8", $msg), >+ contenttype => 'text/plain; charset="utf8"', >+ } >+ ); >+ >+ # Send or die. >+ die($Mail::Sendmail::error) >+ unless Mail::Sendmail::sendmail(%mail); >+ } >+ >+ } else { # Else emit to stdout. >+ printf $msg; >+ } >+ } >+} >+ >+#### Main Code >+ >+# Compile Stockrotation Report data >+my $rotas = Koha::StockRotationRotas->search; >+my $data = $rotas->investigate; >+ >+# Perform db updates if requested >+execute($data) if ( $execute ); >+ >+# Emit Reports >+my $out_report = {}; >+$out_report = report_email($data, $branch) if $report eq 'email'; >+$out_report = report_full($data, $branch) if $report eq 'full'; >+emit({ >+ admin_email => $admin_email, >+ execute => $execute, >+ report => $out_report, >+ send_all => $send_all, >+ send_email => $send_email, >+}); >+ >+=head1 AUTHOR >+ >+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> >+ >+=cut >diff --git a/t/db_dependent/Items.t b/t/db_dependent/Items.t >index 8c85959bb7..fb5ca76b13 100755 >--- a/t/db_dependent/Items.t >+++ b/t/db_dependent/Items.t >@@ -30,7 +30,7 @@ use t::lib::TestBuilder; > use Koha::MarcSubfieldStructures; > use Koha::Caches; > >-use Test::More tests => 12; >+use Test::More tests => 14; > > use Test::Warn; > >@@ -758,6 +758,69 @@ subtest 'get_hostitemnumbers_of' => sub { > is( @itemnumbers, 0, ); > }; > >+subtest 'Check stockrotationitem relationship' => sub { >+ plan tests => 1; >+ >+ $schema->storage->txn_begin(); >+ >+ my $builder = t::lib::TestBuilder->new; >+ my $item = $builder->build({ source => 'Item' }); >+ >+ $builder->build({ >+ source => 'Stockrotationitem', >+ value => { itemnumber_id => $item->{itemnumber} } >+ }); >+ >+ my $sritem = Koha::Items->find($item->{itemnumber})->stockrotationitem; >+ isa_ok( $sritem, 'Koha::StockRotationItem', "Relationship works and correctly creates Koha::Object." ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest 'Check add_to_rota method' => sub { >+ plan tests => 2; >+ >+ $schema->storage->txn_begin(); >+ >+ my $builder = t::lib::TestBuilder->new; >+ my $item = $builder->build({ source => 'Item' }); >+ my $rota = $builder->build({ source => 'Stockrotationrota' }); >+ my $srrota = Koha::StockRotationRotas->find($rota->{rota_id}); >+ >+ $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ >+ my $sritem = Koha::Items->find($item->{itemnumber}); >+ $sritem->add_to_rota($rota->{rota_id}); >+ >+ is( >+ Koha::StockRotationItems->find($item->{itemnumber})->stage_id, >+ $srrota->stockrotationstages->next->stage_id, >+ "Adding to a rota a new sritem item being assigned to its first stage." >+ ); >+ >+ my $newrota = $builder->build({ source => 'Stockrotationrota' }); >+ >+ my $srnewrota = Koha::StockRotationRotas->find($newrota->{rota_id}); >+ >+ $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $newrota->{rota_id} }, >+ }); >+ >+ $sritem->add_to_rota($newrota->{rota_id}); >+ >+ is( >+ Koha::StockRotationItems->find($item->{itemnumber})->stage_id, >+ $srnewrota->stockrotationstages->next->stage_id, >+ "Moving an item results in that sritem being assigned to the new first stage." >+ ); >+ >+ $schema->storage->txn_rollback; >+}; >+ > # Helper method to set up a Biblio. > sub get_biblio { > my ( $frameworkcode ) = @_; >diff --git a/t/db_dependent/Koha/Libraries.t b/t/db_dependent/Koha/Libraries.t >index 22c704786b..911903108f 100644 >--- a/t/db_dependent/Koha/Libraries.t >+++ b/t/db_dependent/Koha/Libraries.t >@@ -19,7 +19,7 @@ > > use Modern::Perl; > >-use Test::More tests => 10; >+use Test::More tests => 12; > > use Koha::Library; > use Koha::Libraries; >@@ -88,6 +88,29 @@ is( Koha::Libraries->search->count, $nb_of_libraries + 1, 'Delete should have de > $retrieved_category_2->delete; > is( Koha::LibraryCategories->search->count, $nb_of_categories + 2, 'Delete should have deleted the library category' ); > >+# Stockrotation relationship testing >+ >+my $new_library_sr = $builder->build({ source => 'Branch' }); >+ >+$builder->build({ >+ source => 'Stockrotationstage', >+ value => { branchcode_id => $new_library_sr->{branchcode} }, >+}); >+$builder->build({ >+ source => 'Stockrotationstage', >+ value => { branchcode_id => $new_library_sr->{branchcode} }, >+}); >+$builder->build({ >+ source => 'Stockrotationstage', >+ value => { branchcode_id => $new_library_sr->{branchcode} }, >+}); >+ >+my $srstages = Koha::Libraries->find($new_library_sr->{branchcode}) >+ ->stockrotationstages; >+is( $srstages->count, 3, 'Correctly fetched stockrotationstages associated with this branch'); >+ >+isa_ok( $srstages->next, 'Koha::StockRotationStage', "Relationship correctly creates Koha::Objects." ); >+ > $schema->storage->txn_rollback; > > subtest '->get_effective_marcorgcode' => sub { >diff --git a/t/db_dependent/StockRotationItems.t b/t/db_dependent/StockRotationItems.t >new file mode 100644 >index 0000000000..2d9bf89a39 >--- /dev/null >+++ b/t/db_dependent/StockRotationItems.t >@@ -0,0 +1,393 @@ >+#!/usr/bin/perl >+ >+# Copyright PTFS Europe 2016 >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use DateTime; >+use DateTime::Duration; >+use Koha::Database; >+use Koha::Item::Transfer; >+use t::lib::TestBuilder; >+ >+use Test::More tests => 8; >+ >+my $schema = Koha::Database->new->schema; >+ >+use_ok('Koha::StockRotationItems'); >+use_ok('Koha::StockRotationItem'); >+ >+my $builder = t::lib::TestBuilder->new; >+ >+subtest 'Basic object tests' => sub { >+ >+ plan tests => 5; >+ >+ $schema->storage->txn_begin; >+ >+ my $itm = $builder->build({ source => 'Item' }); >+ my $stage = $builder->build({ source => 'Stockrotationstage' }); >+ >+ my $item = $builder->build({ >+ source => 'Stockrotationitem', >+ value => { >+ itemnumber_id => $itm->{itemnumber}, >+ stage_id => $stage->{stage_id}, >+ }, >+ }); >+ >+ my $sritem = Koha::StockRotationItems->find($item->{itemnumber_id}); >+ isa_ok( >+ $sritem, >+ 'Koha::StockRotationItem', >+ "Correctly create and load a stock rotation item." >+ ); >+ >+ # Relationship to rota >+ isa_ok( $sritem->itemnumber, 'Koha::Item', "Fetched related item." ); >+ is( $sritem->itemnumber->itemnumber, $itm->{itemnumber}, "Related rota OK." ); >+ >+ # Relationship to stage >+ isa_ok( $sritem->stage, 'Koha::StockRotationStage', "Fetched related stage." ); >+ is( $sritem->stage->stage_id, $stage->{stage_id}, "Related stage OK." ); >+ >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest 'Tests for needs_repatriating' => sub { >+ >+ plan tests => 4; >+ >+ $schema->storage->txn_begin; >+ >+ # Setup a pristine stockrotation context. >+ my $sritem = $builder->build({ source => 'Stockrotationitem' }); >+ my $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ $dbitem->itemnumber->homebranch($dbitem->stage->branchcode_id); >+ $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id); >+ $dbitem->stage->position(1); >+ >+ my $dbrota = $dbitem->stage->rota; >+ my $newstage = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { >+ rota_id => $dbrota->rota_id, >+ position => 2, >+ } >+ }); >+ >+ # - homebranch == holdingbranch [0] >+ is( >+ $dbitem->needs_repatriating, 0, >+ "Homebranch == Holdingbranch." >+ ); >+ >+ my $branch = $builder->build({ source => 'Branch' }); >+ $dbitem->itemnumber->holdingbranch($branch->{branchcode}); >+ >+ # - homebranch != holdingbranch [1] >+ is( >+ $dbitem->needs_repatriating, 1, >+ "Homebranch != holdingbranch." >+ ); >+ >+ # Set to incorrect homebranch. >+ $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id); >+ $dbitem->itemnumber->homebranch($branch->{branchcode}); >+ # - homebranch != stockrotationstage.branch & not in transit [1] >+ is( >+ $dbitem->needs_repatriating, 1, >+ "Homebranch != StockRotationStage.Branchcode_id & not in transit." >+ ); >+ >+ # Set to in transit (by implication). >+ $dbitem->stage($newstage->{stage_id}); >+ # - homebranch != stockrotaitonstage.branch & in transit [0] >+ is( >+ $dbitem->needs_repatriating, 1, >+ "homebranch != stockrotaitonstage.branch & in transit." >+ ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest "Tests for repatriate." => sub { >+ plan tests => 3; >+ $schema->storage->txn_begin; >+ my $sritem = $builder->build({ source => 'Stockrotationitem' }); >+ my $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ $dbitem->stage->position(1); >+ $dbitem->stage->duration(50); >+ my $branch = $builder->build({ source => 'Branch' }); >+ $dbitem->itemnumber->holdingbranch($branch->{branchcode}); >+ >+ # Test a straight up repatriate >+ ok($dbitem->repatriate, "Repatriation done."); >+ my $intransfer = $dbitem->itemnumber->get_transfer; >+ is($intransfer->frombranch, $branch->{branchcode}, "Origin correct."); >+ is($intransfer->tobranch, $dbitem->stage->branchcode_id, "Target Correct."); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest "Tests for needs_advancing." => sub { >+ plan tests => 6; >+ $schema->storage->txn_begin; >+ >+ # Test behaviour of item freshly added to rota. >+ my $sritem = $builder->build({ >+ source => 'Stockrotationitem', >+ value => { 'fresh' => 1, }, >+ }); >+ my $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ is($dbitem->needs_advancing, 1, "An item that is fresh will always need advancing."); >+ >+ # Setup a pristine stockrotation context. >+ $sritem = $builder->build({ >+ source => 'Stockrotationitem', >+ value => { 'fresh' => 0,} >+ }); >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ $dbitem->itemnumber->homebranch($dbitem->stage->branchcode_id); >+ $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id); >+ $dbitem->stage->position(1); >+ $dbitem->stage->duration(50); >+ >+ my $dbtransfer = Koha::Item::Transfer->new({ >+ 'itemnumber' => $dbitem->itemnumber_id, >+ 'frombranch' => $dbitem->stage->branchcode_id, >+ 'tobranch' => $dbitem->stage->branchcode_id, >+ 'datesent' => DateTime->now, >+ 'datearrived' => undef, >+ 'comments' => "StockrotationAdvance", >+ })->store; >+ >+ # Test item will not be advanced if in transit. >+ is($dbitem->needs_advancing, 0, "Not ready to advance: in transfer."); >+ # Test item will not be advanced if in transit even if fresh. >+ $dbitem->fresh(1)->store; >+ is($dbitem->needs_advancing, 0, "Not ready to advance: in transfer (fresh)."); >+ $dbitem->fresh(0)->store; >+ >+ # Test item will not be advanced if it has not spent enough time. >+ $dbtransfer->datearrived(DateTime->now)->store; >+ is($dbitem->needs_advancing, 0, "Not ready to advance: Not spent enough time."); >+ # Test item will be advanced if it has not spent enough time, but is fresh. >+ $dbitem->fresh(1)->store; >+ is($dbitem->needs_advancing, 1, "Advance: Not spent enough time, but fresh."); >+ $dbitem->fresh(0)->store; >+ >+ # Test item will be advanced if it has spent enough time. >+ $dbtransfer->datesent( # Item was sent 100 days ago... >+ DateTime->now - DateTime::Duration->new( days => 100 ) >+ )->store; >+ $dbtransfer->datearrived( # And arrived 75 days ago. >+ DateTime->now - DateTime::Duration->new( days => 75 ) >+ )->store; >+ is($dbitem->needs_advancing, 1, "Ready to be advanced."); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest "Tests for advance." => sub { >+ plan tests => 15; >+ $schema->storage->txn_begin; >+ >+ my $sritem = $builder->build({ >+ source => 'Stockrotationitem', >+ value => { 'fresh' => 1 } >+ }); >+ my $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id); >+ my $dbstage = $dbitem->stage; >+ $dbstage->position(1)->duration(50)->store; # Configure stage. >+ # Configure item >+ $dbitem->itemnumber->holdingbranch($dbstage->branchcode_id)->store; >+ $dbitem->itemnumber->homebranch($dbstage->branchcode_id)->store; >+ # Sanity check >+ is($dbitem->stage->stage_id, $dbstage->stage_id, "Stage sanity check."); >+ >+ # Test if an item is fresh, always move to first stage. >+ is($dbitem->fresh, 1, "Fresh is correct."); >+ $dbitem->advance; >+ is($dbitem->stage->stage_id, $dbstage->stage_id, "Stage is first stage after fresh advance."); >+ is($dbitem->fresh, 0, "Fresh reset after advance."); >+ >+ # Test cases of single stage >+ $dbstage->rota->cyclical(1)->store; # Set Rota to cyclical. >+ ok($dbitem->advance, "Single stage cyclical advance done."); >+ ## Refetch dbitem >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ is($dbitem->stage->stage_id, $dbstage->stage_id, "Single stage cyclical stage OK."); >+ >+ # Test with indemand advance >+ $dbitem->indemand(1)->store; >+ ok($dbitem->advance, "Indemand item advance done."); >+ ## Refetch dbitem >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ is($dbitem->indemand, 0, "Indemand OK."); >+ is($dbitem->stage->stage_id, $dbstage->stage_id, "Indemand item advance stage OK."); >+ >+ # Multi stages >+ my $srstage = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { duration => 50 } >+ }); >+ my $dbstage2 = Koha::StockRotationStages->find($srstage->{stage_id}); >+ $dbstage2->move_to_group($dbitem->stage->rota_id); >+ $dbstage2->move_last; >+ >+ # Test a straight up advance >+ ok($dbitem->advance, "Advancement done."); >+ ## Refetch dbitem >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ ## Test results >+ is($dbitem->stage->stage_id, $dbstage2->stage_id, "Stage updated."); >+ my $intransfer = $dbitem->itemnumber->get_transfer; >+ is($intransfer->frombranch, $dbstage->branchcode_id, "Origin correct."); >+ is($intransfer->tobranch, $dbstage2->branchcode_id, "Target Correct."); >+ >+ $dbstage->rota->cyclical(0)->store; # Set Rota to non-cyclical. >+ >+ # Arrive at new branch >+ $intransfer->datearrived(DateTime->now)->store; >+ $dbitem->itemnumber->holdingbranch($srstage->{branchcode_id})->store; >+ $dbitem->itemnumber->homebranch($srstage->{branchcode_id})->store; >+ >+ # Advance again, Remove from rota. >+ ok($dbitem->advance, "Non-cyclical advance."); >+ ## Refetch dbitem >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ is($dbitem, undef, "StockRotationItem has been removed."); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest "Tests for investigate (singular)." => sub { >+ plan tests => 7; >+ $schema->storage->txn_begin; >+ >+ # Test brand new item's investigation ['initiation'] >+ my $sritem = $builder->build({ source => 'Stockrotationitem', value => { fresh => 1 } }); >+ my $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ is($dbitem->investigate->{reason}, 'initiation', "fresh item initiates."); >+ >+ # Test brand new item at stagebranch ['initiation'] >+ $sritem = $builder->build({ source => 'Stockrotationitem', value => { fresh => 1 } }); >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ $dbitem->itemnumber->homebranch($dbitem->stage->branchcode_id)->store; >+ $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id)->store; >+ is($dbitem->investigate->{reason}, 'initiation', "fresh item at stagebranch initiates."); >+ >+ # Test item not at stagebranch with branchtransfer history ['repatriation'] >+ $sritem = $builder->build({ >+ source => 'Stockrotationitem', >+ value => { 'fresh' => 0,} >+ }); >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ my $dbtransfer = Koha::Item::Transfer->new({ >+ 'itemnumber' => $dbitem->itemnumber_id, >+ 'frombranch' => $dbitem->itemnumber->homebranch, >+ 'tobranch' => $dbitem->itemnumber->homebranch, >+ 'datesent' => DateTime->now, >+ 'datearrived' => DateTime->now, >+ 'comments' => "StockrotationAdvance", >+ })->store; >+ is($dbitem->investigate->{reason}, 'repatriation', "older item repatriates."); >+ >+ # Test item at stagebranch with branchtransfer history ['not-ready'] >+ $sritem = $builder->build({ >+ source => 'Stockrotationitem', >+ value => { 'fresh' => 0,} >+ }); >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ $dbtransfer = Koha::Item::Transfer->new({ >+ 'itemnumber' => $dbitem->itemnumber_id, >+ 'frombranch' => $dbitem->itemnumber->homebranch, >+ 'tobranch' => $dbitem->stage->branchcode_id, >+ 'datesent' => DateTime->now, >+ 'datearrived' => DateTime->now, >+ 'comments' => "StockrotationAdvance", >+ })->store; >+ $dbitem->itemnumber->homebranch($dbitem->stage->branchcode_id)->store; >+ $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id)->store; >+ is($dbitem->investigate->{reason}, 'not-ready', "older item at stagebranch not-ready."); >+ >+ # Test item due for advancement ['advancement'] >+ $sritem = $builder->build({ source => 'Stockrotationitem', value => { fresh => 0 } }); >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ $dbitem->indemand(0)->store; >+ $dbitem->stage->duration(50)->store; >+ my $sent_duration = DateTime::Duration->new( days => 55); >+ my $arrived_duration = DateTime::Duration->new( days => 52); >+ $dbtransfer = Koha::Item::Transfer->new({ >+ 'itemnumber' => $dbitem->itemnumber_id, >+ 'frombranch' => $dbitem->itemnumber->homebranch, >+ 'tobranch' => $dbitem->stage->branchcode_id, >+ 'datesent' => DateTime->now - $sent_duration, >+ 'datearrived' => DateTime->now - $arrived_duration, >+ 'comments' => "StockrotationAdvance", >+ })->store; >+ $dbitem->itemnumber->homebranch($dbitem->stage->branchcode_id)->store; >+ $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id)->store; >+ is($dbitem->investigate->{reason}, 'advancement', >+ "Item ready for advancement."); >+ >+ # Test item due for advancement but in-demand ['in-demand'] >+ $sritem = $builder->build({ source => 'Stockrotationitem', value => { fresh => 0 } }); >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ $dbitem->indemand(1)->store; >+ $dbitem->stage->duration(50)->store; >+ $sent_duration = DateTime::Duration->new( days => 55); >+ $arrived_duration = DateTime::Duration->new( days => 52); >+ $dbtransfer = Koha::Item::Transfer->new({ >+ 'itemnumber' => $dbitem->itemnumber_id, >+ 'frombranch' => $dbitem->itemnumber->homebranch, >+ 'tobranch' => $dbitem->stage->branchcode_id, >+ 'datesent' => DateTime->now - $sent_duration, >+ 'datearrived' => DateTime->now - $arrived_duration, >+ 'comments' => "StockrotationAdvance", >+ })->store; >+ $dbitem->itemnumber->homebranch($dbitem->stage->branchcode_id)->store; >+ $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id)->store; >+ is($dbitem->investigate->{reason}, 'in-demand', >+ "Item advances, but in-demand."); >+ >+ # Test item ready for advancement, but at wrong library ['repatriation'] >+ $sritem = $builder->build({ source => 'Stockrotationitem', value => { fresh => 0 } }); >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ $dbitem->indemand(0)->store; >+ $dbitem->stage->duration(50)->store; >+ $sent_duration = DateTime::Duration->new( days => 55); >+ $arrived_duration = DateTime::Duration->new( days => 52); >+ $dbtransfer = Koha::Item::Transfer->new({ >+ 'itemnumber' => $dbitem->itemnumber_id, >+ 'frombranch' => $dbitem->itemnumber->homebranch, >+ 'tobranch' => $dbitem->stage->branchcode_id, >+ 'datesent' => DateTime->now - $sent_duration, >+ 'datearrived' => DateTime->now - $arrived_duration, >+ 'comments' => "StockrotationAdvance", >+ })->store; >+ is($dbitem->investigate->{reason}, 'repatriation', >+ "Item advances, but not at stage branch."); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+1; >diff --git a/t/db_dependent/StockRotationRotas.t b/t/db_dependent/StockRotationRotas.t >new file mode 100644 >index 0000000000..f1b2aa6d78 >--- /dev/null >+++ b/t/db_dependent/StockRotationRotas.t >@@ -0,0 +1,175 @@ >+#!/usr/bin/perl >+ >+# Copyright PTFS Europe 2016 >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Koha::Database; >+use t::lib::TestBuilder; >+ >+use Test::More tests => 5; >+ >+my $schema = Koha::Database->new->schema; >+ >+use_ok('Koha::StockRotationRotas'); >+use_ok('Koha::StockRotationRota'); >+ >+subtest 'Basic object tests' => sub { >+ >+ plan tests => 5; >+ >+ $schema->storage->txn_begin; >+ >+ my $builder = t::lib::TestBuilder->new; >+ >+ my $rota = $builder->build({ source => 'Stockrotationrota' }); >+ >+ my $srrota = Koha::StockRotationRotas->find($rota->{rota_id}); >+ isa_ok( >+ $srrota, >+ 'Koha::StockRotationRota', >+ "Correctly create and load a stock rotation rota." >+ ); >+ >+ $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ >+ my $srstages = $srrota->stockrotationstages; >+ is( $srstages->count, 3, 'Correctly fetched stockrotationstages associated with this rota'); >+ >+ isa_ok( $srstages->next, 'Koha::StockRotationStage', "Relationship correctly creates Koha::Objects." ); >+ >+ #### Test add_item >+ >+ my $item = $builder->build({ source => 'Item' }); >+ >+ $srrota->add_item($item->{itemnumber}); >+ >+ is( >+ Koha::StockRotationItems->find($item->{itemnumber})->stage_id, >+ $srrota->first_stage->stage_id, >+ "Adding an item results in a new sritem item being assigned to the first stage." >+ ); >+ >+ my $newrota = $builder->build({ source => 'Stockrotationrota' }); >+ >+ my $srnewrota = Koha::StockRotationRotas->find($newrota->{rota_id}); >+ >+ $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $newrota->{rota_id} }, >+ }); >+ >+ $srnewrota->add_item($item->{itemnumber}); >+ >+ is( >+ Koha::StockRotationItems->find($item->{itemnumber})->stage_id, >+ $srnewrota->stockrotationstages->next->stage_id, >+ "Moving an item results in that sritem being assigned to the new first stage." >+ ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest '->first_stage test' => sub { >+ plan tests => 2; >+ >+ $schema->storage->txn_begin; >+ >+ my $builder = t::lib::TestBuilder->new; >+ >+ my $rota = $builder->build({ source => 'Stockrotationrota' }); >+ >+ my $stage1 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ my $stage2 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ my $stage3 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ >+ my $srrota = Koha::StockRotationRotas->find($rota->{rota_id}); >+ my $srstage2 = Koha::StockRotationStages->find($stage2->{stage_id}); >+ my $firststage = $srstage2->first_sibling || $srstage2; >+ >+ is( $srrota->first_stage->stage_id, $firststage->stage_id, "First stage works" ); >+ >+ $srstage2->move_first; >+ >+ is( Koha::StockRotationRotas->find($rota->{rota_id})->first_stage->stage_id, $stage2->{stage_id}, "Stage re-organized" ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest '->items test' => sub { >+ plan tests => 1; >+ >+ $schema->storage->txn_begin; >+ >+ my $builder = t::lib::TestBuilder->new; >+ >+ my $rota = $builder->build({ source => 'Stockrotationrota' }); >+ >+ my $stage1 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ my $stage2 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ my $stage3 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ >+ map { $builder->build({ >+ source => 'Stockrotationitem', >+ value => { stage_id => $_ }, >+ }) } ( >+ $stage1->{stage_id}, $stage1->{stage_id}, >+ $stage2->{stage_id}, $stage2->{stage_id}, >+ $stage3->{stage_id}, $stage3->{stage_id}, >+ ); >+ >+ my $srrota = Koha::StockRotationRotas->find($rota->{rota_id}); >+ >+ is( >+ $srrota->stockrotationitems->count, >+ 6, "Correct number of items" >+ ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+1; >diff --git a/t/db_dependent/StockRotationStages.t b/t/db_dependent/StockRotationStages.t >new file mode 100644 >index 0000000000..e6ea69315e >--- /dev/null >+++ b/t/db_dependent/StockRotationStages.t >@@ -0,0 +1,377 @@ >+#!/usr/bin/perl >+ >+# Copyright PTFS Europe 2016 >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Koha::Database; >+use t::lib::TestBuilder; >+ >+use Test::More tests => 6; >+ >+my $schema = Koha::Database->new->schema; >+ >+use_ok('Koha::StockRotationStages'); >+use_ok('Koha::StockRotationStage'); >+ >+my $builder = t::lib::TestBuilder->new; >+ >+subtest 'Basic object tests' => sub { >+ plan tests => 5; >+ >+ $schema->storage->txn_begin; >+ >+ my $library = $builder->build({ source => 'Branch' }); >+ my $rota = $builder->build({ source => 'Stockrotationrota' }); >+ my $stage = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { >+ branchcode_id => $library->{branchcode}, >+ rota_id => $rota->{rota_id}, >+ }, >+ }); >+ >+ my $srstage = Koha::StockRotationStages->find($stage->{stage_id}); >+ isa_ok( >+ $srstage, >+ 'Koha::StockRotationStage', >+ "Correctly create and load a stock rotation stage." >+ ); >+ >+ # Relationship to library >+ isa_ok( $srstage->branchcode, 'Koha::Library', "Fetched related branch." ); >+ is( $srstage->branchcode->branchcode, $library->{branchcode}, "Related branch OK." ); >+ >+ # Relationship to rota >+ isa_ok( $srstage->rota, 'Koha::StockRotationRota', "Fetched related rota." ); >+ is( $srstage->rota->rota_id, $rota->{rota_id}, "Related rota OK." ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest 'DBIx::Class::Ordered tests' => sub { >+ plan tests => 33; >+ >+ $schema->storage->txn_begin; >+ >+ my $library = $builder->build({ source => 'Branch' }); >+ my $rota = $builder->build({ source => 'Stockrotationrota' }); >+ my $stagefirst = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id}, position => 1 } >+ }); >+ my $stageprevious = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id}, position => 2 } >+ }); >+ my $stage = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id}, position => 3 }, >+ }); >+ my $stagenext = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id}, position => 4 } >+ }); >+ my $stagelast = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id}, position => 5 } >+ }); >+ >+ my $srstage = Koha::StockRotationStages->find($stage->{stage_id}); >+ >+ is($srstage->siblings->count, 4, "Siblings works."); >+ is($srstage->previous_siblings->count, 2, "Previous Siblings works."); >+ is($srstage->next_siblings->count, 2, "Next Siblings works."); >+ >+ my $map = { >+ first_sibling => $stagefirst, >+ previous_sibling => $stageprevious, >+ next_sibling => $stagenext, >+ last_sibling => $stagelast, >+ }; >+ # Test plain relations: >+ while ( my ( $srxsr, $check ) = each %{$map} ) { >+ my $sr = $srstage->$srxsr; >+ isa_ok($sr, 'Koha::StockRotationStage', "Fetched using '$srxsr'."); >+ is($sr->stage_id, $check->{stage_id}, "'$srxsr' data is correct."); >+ }; >+ >+ # Test mutators >+ ## Move Previous >+ ok($srstage->move_previous, "Previous."); >+ is($srstage->previous_sibling->stage_id, $stagefirst->{stage_id}, "Previous, correct previous."); >+ is($srstage->next_sibling->stage_id, $stageprevious->{stage_id}, "Previous, correct next."); >+ ## Move Next >+ ok($srstage->move_next, "Back to middle."); >+ is($srstage->previous_sibling->stage_id, $stageprevious->{stage_id}, "Middle, correct previous."); >+ is($srstage->next_sibling->stage_id, $stagenext->{stage_id}, "Middle, correct next."); >+ ## Move First >+ ok($srstage->move_first, "First."); >+ is($srstage->previous_sibling, 0, "First, correct previous."); >+ is($srstage->next_sibling->stage_id, $stagefirst->{stage_id}, "First, correct next."); >+ ## Move Last >+ ok($srstage->move_last, "Last."); >+ is($srstage->previous_sibling->stage_id, $stagelast->{stage_id}, "Last, correct previous."); >+ is($srstage->next_sibling, 0, "Last, correct next."); >+ ## Move To >+ >+ ### Out of range moves. >+ is( >+ $srstage->move_to($srstage->siblings->count + 2), >+ 0, "Move above count of stages." >+ ); >+ is($srstage->move_to(0), 0, "Move to 0th position."); >+ is($srstage->move_to(-1), 0, "Move to negative position."); >+ >+ ### Move To >+ ok($srstage->move_to(3), "Move."); >+ is($srstage->previous_sibling->stage_id, $stageprevious->{stage_id}, "Move, correct previous."); >+ is($srstage->next_sibling->stage_id, $stagenext->{stage_id}, "Move, correct next."); >+ >+ # Group manipulation >+ my $newrota = $builder->build({ source => 'Stockrotationrota' }); >+ ok($srstage->move_to_group($newrota->{rota_id}), "Move to Group."); >+ is(Koha::StockRotationStages->find($srstage->stage_id)->rota_id, $newrota->{rota_id}, "Moved correctly."); >+ >+ # Delete in ordered context >+ ok($srstage->delete, "Deleted OK."); >+ is( >+ Koha::StockRotationStages->find($stageprevious)->next_sibling->stage_id, >+ $stagenext->{stage_id}, >+ "Delete, correctly re-ordered." >+ ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest 'Relationship to stockrotationitems' => sub { >+ plan tests => 2; >+ >+ $schema->storage->txn_begin; >+ my $stage = $builder->build({ source => 'Stockrotationstage' }); >+ >+ $builder->build({ >+ source => 'Stockrotationitem', >+ value => { stage_id => $stage->{stage_id} }, >+ }); >+ $builder->build({ >+ source => 'Stockrotationitem', >+ value => { stage_id => $stage->{stage_id} }, >+ }); >+ $builder->build({ >+ source => 'Stockrotationitem', >+ value => { stage_id => $stage->{stage_id} }, >+ }); >+ >+ my $srstage = Koha::StockRotationStages->find($stage->{stage_id}); >+ my $sritems = $srstage->stockrotationitems; >+ is( >+ $sritems->count, 3, >+ 'Correctly fetched stockrotationitems associated with this stage' >+ ); >+ >+ isa_ok( >+ $sritems->next, 'Koha::StockRotationItem', >+ "Relationship correctly creates Koha::Objects." >+ ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+ >+subtest 'Tests for investigate (singular)' => sub { >+ >+ plan tests => 3; >+ >+ # In this subtest series we will primarily be testing whether items end up >+ # in the correct 'branched' section of the stage-report. We don't care >+ # for item reasons here, as they are tested in StockRotationItems. >+ >+ # We will run tests on first on an empty report (the base-case) and then >+ # on a populated report. >+ >+ # We will need: >+ # - Libraries which will hold the Items >+ # - Rota Which containing the related stages >+ # + Stages on which we run investigate >+ # * Items on the stages >+ >+ $schema->storage->txn_begin; >+ >+ # Libraries >+ my $library1 = $builder->build({ source => 'Branch' }); >+ my $library2 = $builder->build({ source => 'Branch' }); >+ my $library3 = $builder->build({ source => 'Branch' }); >+ >+ my $stage1lib = $builder->build({ source => 'Branch' }); >+ my $stage2lib = $builder->build({ source => 'Branch' }); >+ my $stage3lib = $builder->build({ source => 'Branch' }); >+ my $stage4lib = $builder->build({ source => 'Branch' }); >+ >+ my $libraries = [ $library1, $library2, $library3, $stage1lib, $stage2lib, >+ $stage3lib, $stage4lib ]; >+ >+ # Rota >+ my $rota = $builder->build({ >+ source => 'Stockrotationrota', >+ value => { cyclical => 0 }, >+ }); >+ >+ # Stages >+ my $stage1 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { >+ rota_id => $rota->{rota_id}, >+ branchcode_id => $stage1lib->{branchcode}, >+ duration => 10, >+ position => 1, >+ }, >+ }); >+ my $stage2 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { >+ rota_id => $rota->{rota_id}, >+ branchcode_id => $stage2lib->{branchcode}, >+ duration => 20, >+ position => 2, >+ }, >+ }); >+ my $stage3 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { >+ rota_id => $rota->{rota_id}, >+ branchcode_id => $stage3lib->{branchcode}, >+ duration => 10, >+ position => 3, >+ }, >+ }); >+ my $stage4 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { >+ rota_id => $rota->{rota_id}, >+ branchcode_id => $stage4lib->{branchcode}, >+ duration => 20, >+ position => 4, >+ }, >+ }); >+ >+ # Test on an empty report. >+ my $spec = { >+ $library1->{branchcode} => 1, >+ $library2->{branchcode} => 1, >+ $library3->{branchcode} => 1, >+ $stage1lib->{branchcode} => 2, >+ $stage2lib->{branchcode} => 1, >+ $stage3lib->{branchcode} => 3, >+ $stage4lib->{branchcode} => 4 >+ }; >+ while ( my ( $code, $count ) = each %{$spec} ) { >+ my $cnt = 0; >+ while ( $cnt < $count ) { >+ my $item = $builder->build({ >+ source => 'Stockrotationitem', >+ value => { >+ stage_id => $stage1->{stage_id}, >+ indemand => 0, >+ fresh => 1, >+ } >+ }); >+ my $dbitem = Koha::StockRotationItems->find($item); >+ $dbitem->itemnumber->homebranch($code) >+ ->holdingbranch($code)->store; >+ $cnt++; >+ } >+ } >+ my $report = Koha::StockRotationStages >+ ->find($stage1->{stage_id})->investigate; >+ my $results = []; >+ foreach my $lib ( @{$libraries} ) { >+ my $items = $report->{branched}->{$lib->{branchcode}}->{items} || []; >+ push @{$results}, >+ scalar @{$items}; >+ } >+ >+ # Items assigned to stag1lib -> log, hence $results[4] = 0; >+ is_deeply( $results, [ 1, 1, 1, 2, 1, 3, 4 ], "Empty report test 1."); >+ >+ # Now we test by adding the next stage's items to the same report. >+ $spec = { >+ $library1->{branchcode} => 3, >+ $library2->{branchcode} => 2, >+ $library3->{branchcode} => 1, >+ $stage1lib->{branchcode} => 4, >+ $stage2lib->{branchcode} => 2, >+ $stage3lib->{branchcode} => 0, >+ $stage4lib->{branchcode} => 3 >+ }; >+ while ( my ( $code, $count ) = each %{$spec} ) { >+ my $cnt = 0; >+ while ( $cnt < $count ) { >+ my $item = $builder->build({ >+ source => 'Stockrotationitem', >+ value => { >+ stage_id => $stage2->{stage_id}, >+ indemand => 0, >+ fresh => 1, >+ } >+ }); >+ my $dbitem = Koha::StockRotationItems->find($item); >+ $dbitem->itemnumber->homebranch($code) >+ ->holdingbranch($code)->store; >+ $cnt++; >+ } >+ } >+ >+ $report = Koha::StockRotationStages >+ ->find($stage2->{stage_id})->investigate($report); >+ $results = []; >+ foreach my $lib ( @{$libraries} ) { >+ my $items = $report->{branched}->{$lib->{branchcode}}->{items} || []; >+ push @{$results}, >+ scalar @{$items}; >+ } >+ is_deeply( $results, [ 4, 3, 2, 6, 3, 3, 7 ], "full report test."); >+ >+ # Carry out db updates >+ foreach my $item (@{$report->{items}}) { >+ my $reason = $item->{reason}; >+ if ( $reason eq 'repatriation' ) { >+ $item->{object}->repatriate; >+ } elsif ( grep { $reason eq $_ } >+ qw/in-demand advancement initiation/ ) { >+ $item->{object}->advance; >+ } >+ } >+ >+ $report = Koha::StockRotationStages >+ ->find($stage1->{stage_id})->investigate; >+ $results = []; >+ foreach my $lib ( @{$libraries} ) { >+ my $items = $report->{branched}->{$lib->{branchcode}}->{items} || []; >+ push @{$results}, >+ scalar @{$items}; >+ } >+ # All items have been 'initiated', which means they are either happily in >+ # transit or happily at the library they are supposed to be. Either way >+ # they will register as 'not-ready' in the stock rotation report. >+ is_deeply( $results, [ 0, 0, 0, 0, 0, 0, 0 ], "All items now in logs."); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+1; >diff --git a/t/db_dependent/api/v1/stockrotationstage.t b/t/db_dependent/api/v1/stockrotationstage.t >new file mode 100644 >index 0000000000..c76e9b1891 >--- /dev/null >+++ b/t/db_dependent/api/v1/stockrotationstage.t >@@ -0,0 +1,172 @@ >+#!/usr/bin/env perl >+ >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Test::More tests => 1; >+use Test::Mojo; >+use Test::Warn; >+ >+use t::lib::TestBuilder; >+use t::lib::Mocks; >+ >+use C4::Auth; >+use Koha::StockRotationStages; >+ >+my $schema = Koha::Database->new->schema; >+my $builder = t::lib::TestBuilder->new; >+ >+# FIXME: sessionStorage defaults to mysql, but it seems to break transaction handling >+# this affects the other REST api tests >+t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' ); >+ >+my $remote_address = '127.0.0.1'; >+my $t = Test::Mojo->new('Koha::REST::V1'); >+ >+subtest 'move() tests' => sub { >+ >+ plan tests => 16; >+ >+ $schema->storage->txn_begin; >+ >+ my ( $unauthorized_borrowernumber, $unauthorized_session_id ) = >+ create_user_and_session( { authorized => 0 } ); >+ my ( $authorized_borrowernumber, $authorized_session_id ) = >+ create_user_and_session( { authorized => 1 } ); >+ >+ my $library1 = $builder->build({ source => 'Branch' }); >+ my $library2 = $builder->build({ source => 'Branch' }); >+ my $rota = $builder->build({ source => 'Stockrotationrota' }); >+ my $stage1 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { >+ branchcode_id => $library1->{branchcode}, >+ rota_id => $rota->{rota_id}, >+ } >+ }); >+ my $stage2 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { >+ branchcode_id => $library2->{branchcode}, >+ rota_id => $rota->{rota_id}, >+ } >+ }); >+ my $rota_id = $rota->{rota_id}; >+ my $stage1_id = $stage1->{stage_id}; >+ >+ # Unauthorized attempt to update >+ my $tx = $t->ua->build_tx( >+ PUT => "/api/v1/rotas/$rota_id/stages/$stage1_id/position" => >+ json => 2 >+ ); >+ $tx->req->cookies( >+ { name => 'CGISESSID', value => $unauthorized_session_id } ); >+ $tx->req->env( { REMOTE_ADDR => $remote_address } ); >+ $t->request_ok($tx)->status_is(403); >+ >+ # Invalid attempt to move a stage on a non-existant rota >+ $tx = $t->ua->build_tx( >+ PUT => "/api/v1/rotas/99999999/stages/$stage1_id/position" => >+ json => 2 >+ ); >+ $tx->req->cookies( >+ { name => 'CGISESSID', value => $authorized_session_id } ); >+ $tx->req->env( { REMOTE_ADDR => $remote_address } ); >+ $t->request_ok($tx)->status_is(404) >+ ->json_is( '/error' => "Not found - Invalid rota or stage ID" ); >+ >+ # Invalid attempt to move an non-existant stage >+ $tx = $t->ua->build_tx( >+ PUT => "/api/v1/rotas/$rota_id/stages/999999999/position" => >+ json => 2 >+ ); >+ $tx->req->cookies( >+ { name => 'CGISESSID', value => $authorized_session_id } ); >+ $tx->req->env( { REMOTE_ADDR => $remote_address } ); >+ $t->request_ok($tx)->status_is(404) >+ ->json_is( '/error' => "Not found - Invalid rota or stage ID" ); >+ >+ # Invalid attempt to move stage to current position >+ my $curr_position = $stage1->{position}; >+ $tx = $t->ua->build_tx( >+ PUT => "/api/v1/rotas/$rota_id/stages/$stage1_id/position" => >+ json => $curr_position >+ ); >+ $tx->req->cookies( >+ { name => 'CGISESSID', value => $authorized_session_id } ); >+ $tx->req->env( { REMOTE_ADDR => $remote_address } ); >+ $t->request_ok($tx)->status_is(400) >+ ->json_is( '/error' => "Bad request - new position invalid" ); >+ >+ # Invalid attempt to move stage to invalid position >+ $tx = $t->ua->build_tx( >+ PUT => "/api/v1/rotas/$rota_id/stages/$stage1_id/position" => >+ json => 99999999 >+ ); >+ $tx->req->cookies( >+ { name => 'CGISESSID', value => $authorized_session_id } ); >+ $tx->req->env( { REMOTE_ADDR => $remote_address } ); >+ $t->request_ok($tx)->status_is(400) >+ ->json_is( '/error' => "Bad request - new position invalid" ); >+ >+ # Valid, authorised move >+ $tx = $t->ua->build_tx( >+ PUT => "/api/v1/rotas/$rota_id/stages/$stage1_id/position" => >+ json => 2 >+ ); >+ $tx->req->cookies( >+ { name => 'CGISESSID', value => $authorized_session_id } ); >+ $tx->req->env( { REMOTE_ADDR => $remote_address } ); >+ $t->request_ok($tx)->status_is(200); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+sub create_user_and_session { >+ >+ my $args = shift; >+ my $flags = ( $args->{authorized} ) ? $args->{authorized} : 0; >+ my $dbh = C4::Context->dbh; >+ >+ my $user = $builder->build( >+ { >+ source => 'Borrower', >+ value => { >+ flags => $flags >+ } >+ } >+ ); >+ >+ # Create a session for the authorized user >+ my $session = C4::Auth::get_session(''); >+ $session->param( 'number', $user->{borrowernumber} ); >+ $session->param( 'id', $user->{userid} ); >+ $session->param( 'ip', '127.0.0.1' ); >+ $session->param( 'lasttime', time() ); >+ $session->flush; >+ >+ if ( $args->{authorized} ) { >+ $dbh->do( " >+ INSERT INTO user_permissions (borrowernumber,module_bit,code) >+ VALUES (?,3,'parameters_remaining_permissions')", undef, >+ $user->{borrowernumber} ); >+ } >+ >+ return ( $user->{borrowernumber}, $session->id ); >+} >+ >+1; >diff --git a/tools/stockrotation.pl b/tools/stockrotation.pl >new file mode 100755 >index 0000000000..19f71ddee0 >--- /dev/null >+++ b/tools/stockrotation.pl >@@ -0,0 +1,526 @@ >+#!/usr/bin/perl >+ >+# Copyright 2016 PTFS-Europe Ltd >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+=head1 stockrotation.pl >+ >+ Script to handle stockrotation. Including rotas, their associated stages >+ and items >+ >+=cut >+ >+use Modern::Perl; >+use CGI; >+ >+use C4::Auth; >+use C4::Context; >+use C4::Output; >+ >+use Koha::Libraries; >+use Koha::StockRotationRotas; >+use Koha::StockRotationItems; >+use Koha::StockRotationStages; >+use Koha::Item; >+use Koha::Util::StockRotation qw(:ALL); >+ >+my $input = new CGI; >+ >+unless (C4::Context->preference('StockRotation')) { >+ # redirect to Intranet home if self-check is not enabled >+ print $input->redirect("/cgi-bin/koha/mainpage.pl"); >+ exit; >+} >+ >+my ( $template, $loggedinuser, $cookie ) = get_template_and_user( >+ { >+ template_name => 'tools/stockrotation.tt', >+ query => $input, >+ type => 'intranet', >+ authnotrequired => 0 >+ } >+); >+ >+# Grab all passed data >+# 'our' since Plack changes the scoping >+# of 'my' >+our %params = $input->Vars(); >+ >+my $op = $params{op}; >+ >+if (!defined $op) { >+ >+ # No operation is supplied, we're just displaying the list of rotas >+ my $rotas = Koha::StockRotationRotas->search->as_list; >+ >+ $template->param( >+ existing_rotas => $rotas, >+ no_op_set => 1 >+ ); >+ >+} elsif ($op eq 'create_edit_rota') { >+ >+ # Edit an existing rota or define a new one >+ my $rota_id = $params{rota_id}; >+ >+ my $rota = {}; >+ >+ if (!defined $rota_id) { >+ >+ # No ID supplied, we're creating a new rota >+ # Create a shell rota hashref >+ $rota = { >+ cyclical => 1, >+ active => 0 >+ }; >+ >+ } else { >+ >+ # ID supplied, we're editing an existing rota >+ $rota = Koha::StockRotationRotas->find($rota_id); >+ >+ } >+ >+ $template->param( >+ rota => $rota, >+ op => $op >+ ); >+ >+} elsif ($op eq 'toggle_rota') { >+ >+ # Find and update the active status of the rota >+ my $rota = Koha::StockRotationRotas->find($params{rota_id}); >+ >+ my $new_active = ($rota->active == 1) ? 0 : 1; >+ >+ $rota->active($new_active)->store; >+ >+ # Return to rotas page >+ print $input->redirect('stockrotation.pl'); >+ >+} elsif ($op eq 'process_rota') { >+ >+ # Get a hashref of the submitted rota data >+ my $rota = get_rota_from_form(); >+ >+ if (!process_rota($rota)) { >+ >+ # The submitted rota was invalid >+ $template->param( >+ error => 'invalid_form', >+ rota => $rota, >+ op => 'create_edit_rota' >+ ); >+ >+ } else { >+ >+ # All was well, return to the rotas list >+ print $input->redirect('stockrotation.pl'); >+ >+ } >+ >+} elsif ($op eq 'manage_stages') { >+ >+ my $rota = Koha::StockRotationRotas->find($params{rota_id}); >+ >+ $template->param( >+ rota => $rota, >+ branches => get_branches(), >+ existing_stages => get_stages($rota), >+ rota_id => $params{rota_id}, >+ op => $op >+ ); >+ >+} elsif ($op eq 'create_edit_stage') { >+ >+ # Edit an existing stage or define a new one >+ my $stage_id = $params{stage_id}; >+ >+ my $rota_id = $params{rota_id}; >+ >+ if (!defined $stage_id) { >+ >+ # No ID supplied, we're creating a new stage >+ $template->param( >+ branches => get_branches(), >+ stage => {}, >+ rota_id => $rota_id, >+ op => $op >+ ); >+ >+ } else { >+ >+ # ID supplied, we're editing an existing stage >+ my $stage = Koha::StockRotationStages->find($stage_id); >+ >+ $template->param( >+ branches => get_branches(), >+ stage => $stage, >+ rota_id => $stage->rota->rota_id, >+ op => $op >+ ); >+ >+ } >+ >+} elsif ($op eq 'confirm_remove_from_rota') { >+ >+ # Get the stage we're deleting >+ $template->param( >+ op => $op, >+ rota_id => $params{rota_id}, >+ stage_id => $params{stage_id}, >+ item_id => $params{item_id} >+ ); >+ >+} elsif ($op eq 'confirm_delete_stage') { >+ >+ # Get the stage we're deleting >+ my $stage = Koha::StockRotationStages->find($params{stage_id}); >+ >+ $template->param( >+ op => $op, >+ stage => $stage >+ ); >+ >+} elsif ($op eq 'delete_stage') { >+ >+ # Get the stage we're deleting >+ my $stage = Koha::StockRotationStages->find($params{stage_id}); >+ >+ # Get the ID of the rota with which this stage is associated >+ # (so we can return to the "Manage stages" page after deletion) >+ my $rota_id = $stage->rota->rota_id; >+ >+ $stage->delete; >+ >+ # Return to the stages list >+ print $input->redirect("?op=manage_stages&rota_id=$rota_id"); >+ >+} elsif ($op eq 'process_stage') { >+ >+ # Get a hashref of the submitted stage data >+ my $stage = get_stage_from_form(); >+ >+ # The rota we're managing >+ my $rota_id = $params{rota_id}; >+ >+ if (!process_stage($stage, $rota_id)) { >+ >+ # The submitted stage was invalid >+ # Get all branches >+ my $branches = get_branches(); >+ >+ $template->param( >+ error => 'invalid_form', >+ all_branches => $branches, >+ stage => $stage, >+ rota_id => $rota_id, >+ op => 'create_edit_stage' >+ ); >+ >+ } else { >+ >+ # All was well, return to the stages list >+ print $input->redirect("?op=manage_stages&rota_id=$rota_id"); >+ >+ } >+ >+} elsif ($op eq 'manage_items') { >+ >+ my $rota = Koha::StockRotationRotas->find($params{rota_id}); >+ >+ # Get all items on this rota, for each prefetch their >+ # stage and biblio objects >+ my $items = Koha::StockRotationItems->search( >+ { 'stage.rota_id' => $params{rota_id} }, >+ { >+ prefetch => { >+ stage => { >+ 'stockrotationitems' => { >+ 'itemnumber' => 'biblionumber' >+ } >+ } >+ } >+ } >+ ); >+ >+ $template->param( >+ rota_id => $params{rota_id}, >+ error => $params{error}, >+ items => $items, >+ branches => get_branches(), >+ stages => get_stages($rota), >+ rota => $rota, >+ op => $op >+ ); >+ >+} elsif ($op eq 'move_to_next_stage') { >+ >+ move_to_next_stage($params{item_id}, $params{stage_id}); >+ >+ # Return to the items list >+ print $input->redirect("?op=manage_items&rota_id=" . $params{rota_id}); >+ >+} elsif ($op eq 'toggle_in_demand') { >+ >+ # Toggle the item's in_demand >+ toggle_indemand($params{item_id}, $params{stage_id}); >+ >+ # Return to the items list >+ print $input->redirect("?op=manage_items&rota_id=".$params{rota_id}); >+ >+} elsif ($op eq 'remove_item_from_stage') { >+ >+ # Remove the item from the stage >+ remove_from_stage($params{item_id}, $params{stage_id}); >+ >+ # Return to the items list >+ print $input->redirect("?op=manage_items&rota_id=".$params{rota_id}); >+ >+} elsif ($op eq 'add_items_to_rota') { >+ >+ # The item's barcode, >+ # which we may or may not have been passed >+ my $barcode = $params{barcode}; >+ >+ # The rota we're adding the item to >+ my $rota_id = $params{rota_id}; >+ >+ # The uploaded file filehandle, >+ # which we may or may not have been passed >+ my $barcode_file = $input->upload("barcodefile"); >+ >+ # We need to create an array of one or more barcodes to >+ # insert >+ my @barcodes = (); >+ >+ # If the barcode input box was populated, use it >+ push @barcodes, $barcode if $barcode; >+ >+ # Only parse the uploaded file if necessary >+ if ($barcode_file) { >+ >+ # Call binmode on the filehandle as we want to set a >+ # UTF-8 layer on it >+ binmode($barcode_file, ":encoding(UTF-8)"); >+ # Parse the file into an array of barcodes >+ while (my $barcode = <$barcode_file>) { >+ $barcode =~ s/\r/\n/g; >+ $barcode =~ s/\n+/\n/g; >+ my @data = split(/\n/, $barcode); >+ push @barcodes, @data; >+ } >+ >+ } >+ >+ # A hashref to hold the status of each barcode >+ my $barcode_status = { >+ ok => [], >+ on_other => [], >+ on_this => [], >+ not_found => [] >+ }; >+ >+ # If we have something to work with, do it >+ get_barcodes_status($rota_id, \@barcodes, $barcode_status) if (@barcodes); >+ >+ # Now we know the status of each barcode, add those that >+ # need it >+ if (scalar @{$barcode_status->{ok}} > 0) { >+ >+ add_items_to_rota($rota_id, $barcode_status->{ok}); >+ >+ } >+ # If we were only passed one barcode and it was successfully >+ # added, redirect back to ourselves, we don't want to display >+ # a report, redirect also if we were passed no barcodes >+ if ( >+ scalar @barcodes == 0 || >+ (scalar @barcodes == 1 && scalar @{$barcode_status->{ok}} == 1) >+ ) { >+ >+ print $input->redirect("?op=manage_items&rota_id=$rota_id"); >+ >+ } else { >+ >+ # Report on the outcome >+ $template->param( >+ barcode_status => $barcode_status, >+ rota_id => $rota_id, >+ op => $op >+ ); >+ >+ } >+ >+} elsif ($op eq 'move_items_to_rota') { >+ >+ # The barcodes of the items we're moving >+ my @move = $input->param('move_item'); >+ >+ foreach my $item(@move) { >+ >+ # The item we're moving >+ my $item = Koha::Items->find($item); >+ >+ # Move it to the new rota >+ $item->add_to_rota($params{rota_id}); >+ >+ } >+ >+ # Return to the items list >+ print $input->redirect("?op=manage_items&rota_id=".$params{rota_id}); >+ >+} >+ >+output_html_with_http_headers $input, $cookie, $template->output; >+ >+sub get_rota_from_form { >+ >+ return { >+ id => $params{id}, >+ title => $params{title}, >+ cyclical => $params{cyclical}, >+ active => $params{active}, >+ description => $params{description} >+ }; >+} >+ >+sub get_stage_from_form { >+ >+ return { >+ stage_id => $params{stage_id}, >+ branchcode => $params{branchcode}, >+ duration => $params{duration} >+ }; >+} >+ >+sub process_rota { >+ >+ my $sub_rota = shift; >+ >+ # Fields we require >+ my @required = ('title','cyclical','active'); >+ >+ # Count of the number of required fields we have >+ my $valid = 0; >+ >+ # Ensure we have everything we require >+ foreach my $req(@required) { >+ >+ if (exists $sub_rota->{$req}) { >+ >+ chomp(my $value = $sub_rota->{$req}); >+ if (length $value > 0) { >+ $valid++; >+ } >+ >+ } >+ >+ } >+ >+ # If we don't have everything we need >+ return 0 if $valid != scalar @required; >+ >+ # Passed validation >+ # Find the rota we're updating >+ my $rota = Koha::StockRotationRotas->find($sub_rota->{id}); >+ >+ if ($rota) { >+ >+ $rota->title( >+ $sub_rota->{title} >+ )->cyclical( >+ $sub_rota->{cyclical} >+ )->active( >+ $sub_rota->{active} >+ )->description( >+ $sub_rota->{description} >+ )->store; >+ >+ } else { >+ >+ $rota = Koha::StockRotationRota->new({ >+ title => $sub_rota->{title}, >+ cyclical => $sub_rota->{cyclical}, >+ active => $sub_rota->{active}, >+ description => $sub_rota->{description} >+ })->store; >+ >+ } >+ >+ return 1; >+} >+ >+sub process_stage { >+ >+ my ($sub_stage, $rota_id) = @_; >+ >+ # Fields we require >+ my @required = ('branchcode','duration'); >+ >+ # Count of the number of required fields we have >+ my $valid = 0; >+ >+ # Ensure we have everything we require >+ foreach my $req(@required) { >+ >+ if (exists $sub_stage->{$req}) { >+ >+ chomp(my $value = $sub_stage->{$req}); >+ if (length $value > 0) { >+ $valid++; >+ } >+ >+ } >+ >+ } >+ >+ # If we don't have everything we need >+ return 0 if $valid != scalar @required; >+ >+ # Passed validation >+ # Find the stage we're updating >+ my $stage = Koha::StockRotationStages->find($sub_stage->{stage_id}); >+ >+ if ($stage) { >+ >+ # Updating an existing stage >+ $stage->branchcode_id( >+ $sub_stage->{branchcode} >+ )->duration( >+ $sub_stage->{duration} >+ )->store; >+ >+ } else { >+ >+ # Creating a new stage >+ $stage = Koha::StockRotationStage->new({ >+ branchcode_id => $sub_stage->{branchcode}, >+ rota_id => $rota_id, >+ duration => $sub_stage->{duration} >+ })->store; >+ >+ } >+ >+ return 1; >+} >+ >+=head1 AUTHOR >+ >+Andrew Isherwood <andrew.isherwood@ptfs-europe.com> >+ >+=cut >-- >2.13.4
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 11897
:
25887
|
58207
|
58208
|
58209
|
58210
|
58211
|
58212
|
58213
|
58214
|
58215
|
58216
|
58217
|
58218
|
58219
|
58280
|
58281
|
58687
|
58692
|
58693
|
58694
|
58695
|
58696
|
60175
|
60715
|
61825
|
61826
|
61827
|
61828
|
61829
|
61830
|
61831
|
61832
|
61833
|
61834
|
61835
|
61836
|
61837
|
61838
|
61839
|
61885
|
62730
|
62750
|
62751
|
62752
|
62753
|
62754
|
62755
|
62756
|
62757
|
62758
|
62759
|
62760
|
62761
|
62762
|
62763
|
62764
|
62765
|
62766
|
62767
|
63063
|
63064
|
63065
|
63066
|
63067
|
63068
|
63069
|
63070
|
63071
|
63072
|
63073
|
63074
|
63075
|
63076
|
63077
|
63078
|
63079
|
63080
|
63114
|
64850
|
64851
|
65715
|
65716
|
65717
|
65718
|
65719
|
65720
|
65721
|
65722
|
65723
|
65724
|
65725
|
65726
|
65727
|
65728
|
65729
|
65730
|
65731
|
65732
|
65733
|
65734
|
65735
|
65736
|
65737
|
65738
|
65739
|
65740
|
65741
|
65742
|
65743
|
65744
|
65745
|
65746
|
65747
|
65748
|
65749
|
65750
|
65751
|
65752
|
65753
|
65754
|
65755
|
65756
|
65757
|
65758
|
66381
|
70496
|
70497
|
70498
|
70499
|
70500
|
70501
|
70502
|
70503
|
70504
|
70505
|
70506
|
70507
|
70508
|
70509
|
70510
|
70511
|
70512
|
70513
|
70514
|
70515
|
70516
|
70517
|
70518
|
70519
|
70520
|
70521
|
70522
|
70523
|
70524
|
72124
|
72125
|
72126
|
72127
|
72128
|
74003
|
74004
|
74005
|
74006
|
74007
|
74008
|
74010
|
74011
|
74012
|
74013
|
74014
|
74015
|
74016
|
74017
|
74197
|
74198
|
74199
|
74200
|
74201
|
74202
|
74203
|
74204
|
74205
|
74206
|
74319
|
74320
|
74360
|
74459
|
74460
|
74461
|
74462
|
74463
|
74464
|
74465
|
74466
|
74467
|
74468
|
74469
|
74470
|
75409
|
75410
|
75411
|
75412
|
75413
|
75414
|
75415
|
75416
|
75417
|
75418
|
75419
|
75420
|
76121
|
76122
|
76123
|
76124
|
76125
|
76126
|
76127
|
76128
|
76129
|
76130
|
76131
|
76132
|
76133
|
76134
|
76135
|
76144
|
76706
|
76707
|
76708
|
76709
|
76710
|
77623
|
77624
|
77625
|
77626
|
78387
|
78388
|
78389
|
78390
|
78391
|
78392
|
78393
|
78394
|
78404
|
78405
|
78406
|
78407
|
78408
|
79040
|
79041
|
79042
|
79043
|
79044
|
79738
|
79739
|
79740
|
79741
|
79742
|
79746
|
79747
|
79748
|
79749
|
79750
|
79964
|
79965
|
79966
|
79967
|
79968
|
79969
|
79970
|
79971
|
79972
|
79973
|
79974
|
79975
|
80068
|
80087
|
80088
|
80089
|
80090
|
80091
|
80092
|
80093
|
80094
|
80095
|
80096
|
80097
|
80098
|
80099
|
80100
|
80118
|
80260
|
80261
|
80262
|
80263
|
80264
|
80265
|
80266
|
80267
|
80268
|
80269
|
80270
|
80271
|
80272
|
80273
|
80274
|
80275
|
80289
|
80298
|
80299
|
80300