From b89653c9f671a0de49b3119da7bb22765489bf98 Mon Sep 17 00:00:00 2001 From: Alex Sassmannshausen Date: Thu, 8 Dec 2016 18:32:04 +0100 Subject: [PATCH] Bug 11897: Stockrotation The stock rotation feature adds a batch process to automate rotation of catalgue items with a staff client page under tools to manage rotas/schedules. Once a rota is configured, and your staff user has the right permissions to allocate items, then an additional tab will appear on biblio records allowing the management of of which rota, if any, individual items belong to. It also includes a cron script to process the items on a daily basis. Signed-off-by: Kathleen Milne --- Koha/Item.pm | 49 +- Koha/Library.pm | 46 ++ Koha/REST/V1/Stage.pm | 60 ++ Koha/StockRotationItem.pm | 273 +++++++++ Koha/StockRotationItems.pm | 128 +++++ Koha/StockRotationRota.pm | 182 ++++++ Koha/StockRotationRotas.pm | 105 ++++ Koha/StockRotationStage.pm | 419 ++++++++++++++ Koha/StockRotationStages.pm | 90 +++ Koha/Util/StockRotation.pm | 247 ++++++++ api/v1/swagger/paths.json | 3 + api/v1/swagger/paths/rotas.json | 79 +++ catalogue/stockrotation.pl | 179 ++++++ .../mysql/en/mandatory/sample_notices.sql | 3 +- installer/data/mysql/kohastructure.sql | 72 +-- installer/data/mysql/sysprefs.sql | 7 +- .../prog/css/src/staff-global.scss | 121 ++++ .../prog/en/includes/biblio-view-menu.inc | 1 + .../prog/en/includes/permissions.inc | 5 +- .../en/includes/stockrotation-toolbar.inc | 12 + .../prog/en/includes/tools-menu.inc | 5 + .../en/modules/catalogue/stockrotation.tt | 171 ++++++ .../prog/en/modules/tools/stockrotation.tt | 510 +++++++++++++++++ .../prog/en/modules/tools/tools-home.tt | 7 + .../prog/js/pages/stockrotation.js | 65 +++ misc/cronjobs/stockrotation.pl | 528 +++++++++++++++++ t/db_dependent/Items.t | 61 ++ 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 | 531 ++++++++++++++++++ 33 files changed, 5056 insertions(+), 45 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 964359ecf8..721c0d66d6 100644 --- a/Koha/Item.pm +++ b/Koha/Item.pm @@ -31,6 +31,8 @@ use Koha::Item::Transfer::Limits; use Koha::Item::Transfers; use Koha::Patrons; use Koha::Libraries; +use Koha::StockRotationItem; +use Koha::StockRotationRotas; use base qw(Koha::Object); @@ -282,7 +284,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 9e6d616036..7877174ec5 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); @@ -41,6 +42,51 @@ TODO: Ask the author to add a proper description =cut +sub get_categories { + my ( $self, $params ) = @_; + # TODO This should return Koha::LibraryCategories + return $self->{_result}->categorycodes( $params ); +} + +=head3 update_categories + +TODO: Ask the author to add a proper description + +=cut + +sub update_categories { + my ( $self, $categories ) = @_; + $self->_result->delete_related( 'branchrelations' ); + $self->add_to_categories( $categories ); +} + +=head3 add_to_categories + +TODO: Ask the author to add a proper description + +=cut + +sub add_to_categories { + my ( $self, $categories ) = @_; + for my $category ( @$categories ) { + $self->_result->add_to_categorycodes( $category->_result ); + } +} + +=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..485384a41b --- /dev/null +++ b/Koha/REST/V1/Stage.pm @@ -0,0 +1,60 @@ +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; + +=head1 NAME + +Koha::REST::V1::Stage + +=head2 Operations + +=head3 move + +Move a stage up or down the stockrotation rota. + +=cut + +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 + +=cut diff --git a/Koha/StockRotationItems.pm b/Koha/StockRotationItems.pm new file mode 100644 index 0000000000..c78c2676c3 --- /dev/null +++ b/Koha/StockRotationItems.pm @@ -0,0 +1,128 @@ +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'; +} + +=head3 object_class + +=cut + +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 + +=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 + +=cut diff --git a/Koha/StockRotationRotas.pm b/Koha/StockRotationRotas.pm new file mode 100644 index 0000000000..ba905ff2ed --- /dev/null +++ b/Koha/StockRotationRotas.pm @@ -0,0 +1,105 @@ +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'; +} + +=head3 object_class + +=cut + +sub object_class { + return 'Koha::StockRotationRota'; +} + +1; + +=head1 AUTHOR + +Alex Sassmannshausen + +=cut diff --git a/Koha/StockRotationStage.pm b/Koha/StockRotationStage.pm new file mode 100644 index 0000000000..dc76ba83f6 --- /dev/null +++ b/Koha/StockRotationStage.pm @@ -0,0 +1,419 @@ +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->branchreplyto + ? $stagebranch->branchreplyto + : $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 => $stagebranch->branchreplyto + ? $stagebranch->branchreplyto + : $stagebranch->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 => $stagebranch->branchreplyto + ? $stagebranch->branchreplyto + : $stagebranch->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 + +=cut diff --git a/Koha/StockRotationStages.pm b/Koha/StockRotationStages.pm new file mode 100644 index 0000000000..1b8599926b --- /dev/null +++ b/Koha/StockRotationStages.pm @@ -0,0 +1,90 @@ +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'; +} + +=head3 object_class + +=cut + +sub object_class { + return 'Koha::StockRotationStage'; +} + +1; + +=head1 AUTHOR + +Alex Sassmannshausen + +=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 + +=cut diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json index c9d8e881b5..c8870db2a1 100644 --- a/api/v1/swagger/paths.json +++ b/api/v1/swagger/paths.json @@ -34,5 +34,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..8b6a32451f --- /dev/null +++ b/catalogue/stockrotation.pl @@ -0,0 +1,179 @@ +#!/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, + stockrotation => 'manage_rota_items', + }, + } +); + +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, + order_by => 'title' + } + ); + + # 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 + +=cut diff --git a/installer/data/mysql/en/mandatory/sample_notices.sql b/installer/data/mysql/en/mandatory/sample_notices.sql index c36458ec26..1891c1f231 100644 --- a/installer/data/mysql/en/mandatory/sample_notices.sql +++ b/installer/data/mysql/en/mandatory/sample_notices.sql @@ -176,8 +176,9 @@ INSERT INTO `letter` (`module`, `code`, `branchcode`, `name`, `is_html`, `title` ('circulation', 'AR_SLIP', '', 'Article request - print slip', 0, 'Article request', 'Article request:\r\n\r\n<> <> (<>),\r\n\r\nTitle: <>\r\nBarcode: <>\r\n\r\nArticle requested:\r\nTitle: <>\r\nAuthor: <>\r\nVolume: <>\r\nIssue: <>\r\nDate: <>\r\nPages: <>\r\nChapters: <>\r\nNotes: <>\r\n', 'print'), ('circulation', 'AR_PROCESSING', '', 'Article request - processing', 0, 'Article request processing', 'Dear <> <> (<>),\r\n\r\nWe are now processing your request for an article from <> (<>).\r\n\r\nArticle requested:\r\nTitle: <>\r\nAuthor: <>\r\nVolume: <>\r\nIssue: <>\r\nDate: <>\r\nPages: <>\r\nChapters: <>\r\nNotes: <>\r\n\r\nThank you!', 'email'), ('circulation', 'CHECKOUT_NOTE', '', 'Checkout note on item set by patron', '0', 'Checkout note', '<> <> has added a note to the item <> - <> (<>).','email'); - INSERT INTO `letter` (`module`, `code`, `branchcode`, `name`, `is_html`, `title`, `content`, `message_transport_type`, `lang`) VALUES ('circulation', 'ACCOUNT_PAYMENT', '', 'Account payment', 0, 'Account payment', '[%- USE Price -%]\r\nA payment of [% credit.amount * -1 | $Price %] has been applied to your account.\r\n\r\nThis payment affected the following fees:\r\n[%- FOREACH o IN offsets %]\r\nDescription: [% o.debit.description %]\r\nAmount paid: [% o.amount * -1 | $Price %]\r\nAmount remaining: [% o.debit.amountoutstanding | $Price %]\r\n[% END %]', 'email', 'default'), ('circulation', 'ACCOUNT_WRITEOFF', '', 'Account writeoff', 0, 'Account writeoff', '[%- USE Price -%]\r\nAn account writeoff of [% credit.amount * -1 | $Price %] has been applied to your account.\r\n\r\nThis writeoff affected the following fees:\r\n[%- FOREACH o IN offsets %]\r\nDescription: [% o.debit.description %]\r\nAmount paid: [% o.amount * -1 | $Price %]\r\nAmount remaining: [% o.debit.amountoutstanding | $Price %]\r\n[% END %]', 'email', 'default'); +INSERT INTO `letter` (`module`, `code`, `branchcode`, `name`, `is_html`, `title`, `content`, `message_transport_type`) VALUES +('circulation', 'SR_SLIP', '', 'Stock Rotation Slip', 0, 'Stockrotation Report', 'Stockrotation report for [% branch.name %]:\r\n\r\n[% IF branch.items.size %][% branch.items.size %] items to be processed for this branch.\r\n[% ELSE %]No items to be processed for this branch\r\n[% END %][% FOREACH item IN branch.items %][% IF item.reason ne \'in-demand\' %]Title: [% item.title %]\r\nAuthor: [% item.author %]\r\nCallnumber: [% item.callnumber %]\r\nLocation: [% item.location %]\r\nBarcode: [% item.barcode %]\r\nOn loan?: [% item.onloan %]\r\nStatus: [% item.reason %]\r\nCurrent Library: [% item.branch.branchname %] [% item.branch.branchcode %]\r\n\r\n[% END %][% END %]', 'email'); diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index 9a5afb149f..ef4ddd38f0 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -4142,40 +4142,6 @@ CREATE TABLE illrequests ( ON UPDATE CASCADE ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; --- --- Table structure for table `stockrotationrotas` --- - -CREATE TABLE IF NOT EXISTS stockrotationrotas ( - rota_id int(11) auto_increment, -- Stockrotation rota ID - title varchar(100) NOT NULL, -- Title for this rota - description text NOT NULL default '', -- Description for this rota - cyclical tinyint(1) NOT NULL default 0, -- Should items on this rota keep cycling? - active tinyint(1) NOT NULL default 0, -- Is this rota currently active? - PRIMARY KEY (`rota_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; - --- --- Table structure for table `stockrotationstages` --- - -CREATE TABLE IF NOT EXISTS stockrotationstages ( - stage_id int(11) auto_increment, -- Unique stage ID - position int(11) NOT NULL, -- The position of this stage within its rota - rota_id int(11) NOT NULL, -- The rota this stage belongs to - branchcode_id varchar(10) NOT NULL, -- Branch this stage relates to - duration int(11) NOT NULL default 4, -- The number of days items shoud occupy this stage - PRIMARY KEY (`stage_id`), - CONSTRAINT `stockrotationstages_rifk` - FOREIGN KEY (`rota_id`) - REFERENCES `stockrotationrotas` (`rota_id`) - ON UPDATE CASCADE ON DELETE CASCADE, - CONSTRAINT `stockrotationstages_bifk` - FOREIGN KEY (`branchcode_id`) - REFERENCES `branches` (`branchcode`) - ON UPDATE CASCADE ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; - -- -- Table structure for table `illrequestattributes` -- @@ -4205,7 +4171,7 @@ CREATE TABLE library_groups ( description MEDIUMTEXT NULL DEFAULT NULL, -- Longer explanation of the group, if necessary ft_hide_patron_info tinyint(1) NOT NULL DEFAULT 0, -- Turn on the feature "Hide patron's info" for this group ft_search_groups_opac tinyint(1) NOT NULL DEFAULT 0, -- Use this group for staff side search groups - ft_search_groups_staff tinyint(1) NOT NULL DEFAULT 0, -- Use this group for opac side search groups + ft_search_groups_staff tinyint(1) NOT NULL DEFAULT 0, -- Use this group for opac side search groups created_on TIMESTAMP NULL, -- Date and time of creation updated_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- Date and time of last PRIMARY KEY id ( id ), @@ -4226,6 +4192,40 @@ CREATE TABLE `oauth_access_tokens` ( PRIMARY KEY (`access_token`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +-- +-- Table structure for table `stockrotationrotas` +-- + +CREATE TABLE IF NOT EXISTS stockrotationrotas ( + rota_id int(11) auto_increment, -- Stockrotation rota ID + title varchar(100) NOT NULL, -- Title for this rota + description text NOT NULL default '', -- Description for this rota + cyclical tinyint(1) NOT NULL default 0, -- Should items on this rota keep cycling? + active tinyint(1) NOT NULL default 0, -- Is this rota currently active? + PRIMARY KEY (`rota_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `stockrotationstages` +-- + +CREATE TABLE IF NOT EXISTS stockrotationstages ( + stage_id int(11) auto_increment, -- Unique stage ID + position int(11) NOT NULL, -- The position of this stage within its rota + rota_id int(11) NOT NULL, -- The rota this stage belongs to + branchcode_id varchar(10) NOT NULL, -- Branch this stage relates to + duration int(11) NOT NULL default 4, -- The number of days items shoud occupy this stage + PRIMARY KEY (`stage_id`), + CONSTRAINT `stockrotationstages_rifk` + FOREIGN KEY (`rota_id`) + REFERENCES `stockrotationrotas` (`rota_id`) + ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT `stockrotationstages_bifk` + FOREIGN KEY (`branchcode_id`) + REFERENCES `branches` (`branchcode`) + ON UPDATE CASCADE ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + -- -- Table structure for table `stockrotationitems` -- @@ -4244,7 +4244,7 @@ CREATE TABLE IF NOT EXISTS stockrotationitems ( FOREIGN KEY (`stage_id`) REFERENCES `stockrotationstages` (`stage_id`) ON UPDATE CASCADE ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql index ff5b43ad72..7fe15e6d52 100644 --- a/installer/data/mysql/sysprefs.sql +++ b/installer/data/mysql/sysprefs.sql @@ -479,6 +479,8 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('reviewson','1','','If ON, enables patron reviews of bibliographic records in the OPAC','YesNo'), ('RisExportAdditionalFields', '', NULL , 'Define additional RIS tags to export from MARC records in YAML format as an associative array with either a marc tag/subfield combination as the value, or a list of tag/subfield combinations.', 'textarea'), ('RoutingListAddReserves','0','','If ON the patrons on routing lists are automatically added to holds on the issue.','YesNo'), +('RotationPreventTransfers','0',NULL,'If ON, prevent any transfers for items on stock rotation rotas, except for stock rotation transfers','YesNo'), +('RoutingListAddReserves','1','','If ON the patrons on routing lists are automatically added to holds on the issue.','YesNo'), ('RoutingListNote','To change this note edit RoutingListNote system preference.','70|10','Define a note to be shown on all routing lists','Textarea'), ('RoutingSerials','1',NULL,'If ON, serials routing is enabled','YesNo'), ('SCOMainUserBlock','','70|10','Add a block of HTML that will display on the self checkout screen','Textarea'), @@ -525,6 +527,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('StaffSerialIssueDisplayCount','3','','Number of serial issues to display per subscription in the Staff client','Integer'), ('StaticHoldsQueueWeight','0',NULL,'Specify a list of library location codes separated by commas -- the list of codes will be traversed and weighted with first values given higher weight for holds fulfillment -- alternatively, if RandomizeHoldsQueueWeight is set, the list will be randomly selective','Integer'), ('StatisticsFields','location|itype|ccode', NULL, 'Define Fields (from the items table) used for statistics members','Free'), +('StockRotation','0',NULL,'If ON, enables the stock rotation module','YesNo'), ('StoreLastBorrower','0','','If ON, the last borrower to return an item will be stored in items.last_returned_by','YesNo'), ('SubfieldsToAllowForRestrictedBatchmod','','Define a list of subfields for which edition is authorized when items_batchmod_restricted permission is enabled, separated by spaces. Example: 995\$f 995\$h 995\$j',NULL,'Free'), ('SubfieldsToAllowForRestrictedEditing','','Define a list of subfields for which edition is authorized when edit_items_restricted permission is enabled, separated by spaces. Example: 995\$f 995\$h 995\$j',NULL,'Free'), @@ -615,7 +618,5 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('XSLTListsDisplay','default','','Enable XSLT stylesheet control over lists pages display on intranet','Free'), ('XSLTResultsDisplay','default','','Enable XSL stylesheet control over results page display on intranet','Free'), ('z3950AuthorAuthFields','701,702,700',NULL,'Define the MARC biblio fields for Personal Name Authorities to fill biblio.author','free'), -('z3950NormalizeAuthor','0','','If ON, Personal Name Authorities will replace authors in biblio.author','YesNo'), -('StockRotation','0',NULL,'If ON, enables the stock rotation module','YesNo'), -('RotationPreventTransfers','0',NULL,'If ON, prevent any transfers for items on stock rotation rotas, except for stock rotation transfers','YesNo') +('z3950NormalizeAuthor','0','','If ON, Personal Name Authorities will replace authors in biblio.author','YesNo') ; diff --git a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss index 1074910e9a..93b0d5b924 100644 --- a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss +++ b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss @@ -4034,6 +4034,127 @@ span { width: 100% !important; } +#stockrotation { + h3 { + margin: 30px 0 10px 0; + } + .dialog { + h3 { + margin: 10px 0; + } + margin-bottom: 20px; + } + .highlight_stage { + font-weight: bold; + } +} + +#catalog_stockrotation .highlight_stage { + font-weight: bold; +} + +#stockrotation { + #rota_form { + textarea { + width: 300px; + height: 100px; + } + #name { + width: 300px; + } + fieldset { + width: auto; + } + } + #stage_form fieldset, #add_rota_item_form fieldset { + width: auto; + } + .dialog.alert { + ul { + margin: 20px 0; + } + li { + list-style-type: none; + } + } +} + +#catalog_stockrotation { + .item_select_rota { + vertical-align: middle; + } + 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; + span { + padding: 3px; + } +} + +#manage_stages { + ul { + padding-left: 0; + } + li { + list-style: none; + margin-bottom: 5px; + span { + padding: 6px 3px; + } + } + .stagename { + width: 15em; + display: inline-block; + } + .stageduration { + width: 10em; + display: inline-block; + } + .stageactions { + display: inline-block; + } + li:nth-child(odd) { + background-color: #F3F3F3; + } + .drag_handle { + margin-right: 6px; + cursor: move; + } + .drag_placeholder { + height: 2em; + border: 1px dotted #aaa; + } + h3 { + display: inline-block; + } + #ajax_status { + display: inline-block; + border: 1px solid #bcbcbc; + border-radius: 5px; + padding: 5px; + margin-left: 10px; + background: #f3f3f3; + } + #manage_stages_help { + margin: 20px 0; + } +} #helper { span { 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 9f5c0cb9e2..29f8d67524 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,5 +40,6 @@ [% IF ( issuehistoryview ) %]
  • [% ELSE %]
  • [% END %] Checkout history
  • [% IF ( CAN_user_tools_view_system_logs ) %][% IF ( logview ) %]
  • [% ELSE %]
  • [% END %]Modification log
  • [% END %] +[% IF ( CAN_user_stockrotation_manage_rota_items && Koha.Preference('StockRotation') ) %][% IF ( stockrotationview ) %]
  • [% ELSE %]
  • [% END %]Rota
  • [% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc index 81efab0c12..76009c87eb 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc @@ -118,8 +118,7 @@ [%# self_check %] [%- CASE 'self_checkin_module' -%]Log into the self check-in module. Note: this permission prevents the patron from using any other OPAC functionality [%- CASE 'self_checkout_module' -%]Perform self checkout at the OPAC. It should be used for the patron matching the AutoSelfCheckID + [%- CASE 'manage_rota_items' -%]Add and remove items from rotas + [%- CASE 'manage_rotas' -%]Create, edit and delete rotas [%- END -%] - [%- CASE 'can_add_items_rotas' -%]Add and remove items from rotas - [%- CASE 'can_edit_rotas' -%]Create, edit and delete rotas - [%- 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..f574b19978 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/stockrotation-toolbar.inc @@ -0,0 +1,12 @@ +[% USE Koha %] +
    + [% IF no_op_set %] + New rota + [% END %] + [% IF op == 'manage_stages' %] + Edit rota + [% END %] + [% IF op == 'manage_items' %] + Edit rota + [% END %] +
    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 d3e03a95ad..bef464c37b 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,5 @@ +[% USE Koha %] +