Bugzilla – Attachment 79044 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: Stockrotation
Bug-11897-Stockrotation.patch (text/plain), 189.54 KB, created by
Martin Renvoize (ashimema)
on 2018-09-18 12:54:23 UTC
(
hide
)
Description:
Bug 11897: Stockrotation
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2018-09-18 12:54:23 UTC
Size:
189.54 KB
patch
obsolete
>From b89653c9f671a0de49b3119da7bb22765489bf98 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: 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 <kathleen.milne@cne-siar.gov.uk> >--- > 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 <alex.sassmannshausen@ptfs-europe.com> >+ >+=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 <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..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 <alex.sassmannshausen@ptfs-europe.com> >+ >+=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 <alex.sassmannshausen@ptfs-europe.com> >+ >+=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 <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 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 <andrew.isherwood@ptfs-europe.com> >+ >+=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<<borrowers.firstname>> <<borrowers.surname>> (<<borrowers.cardnumber>>),\r\n\r\nTitle: <<biblio.title>>\r\nBarcode: <<items.barcode>>\r\n\r\nArticle requested:\r\nTitle: <<article_requests.title>>\r\nAuthor: <<article_requests.author>>\r\nVolume: <<article_requests.volume>>\r\nIssue: <<article_requests.issue>>\r\nDate: <<article_requests.date>>\r\nPages: <<article_requests.pages>>\r\nChapters: <<article_requests.chapters>>\r\nNotes: <<article_requests.patron_notes>>\r\n', 'print'), > ('circulation', 'AR_PROCESSING', '', 'Article request - processing', 0, 'Article request processing', 'Dear <<borrowers.firstname>> <<borrowers.surname>> (<<borrowers.cardnumber>>),\r\n\r\nWe are now processing your request for an article from <<biblio.title>> (<<items.barcode>>).\r\n\r\nArticle requested:\r\nTitle: <<article_requests.title>>\r\nAuthor: <<article_requests.author>>\r\nVolume: <<article_requests.volume>>\r\nIssue: <<article_requests.issue>>\r\nDate: <<article_requests.date>>\r\nPages: <<article_requests.pages>>\r\nChapters: <<article_requests.chapters>>\r\nNotes: <<article_requests.patron_notes>>\r\n\r\nThank you!', 'email'), > ('circulation', 'CHECKOUT_NOTE', '', 'Checkout note on item set by patron', '0', 'Checkout note', '<<borrowers.firstname>> <<borrowers.surname>> has added a note to the item <<biblio.title>> - <<biblio.author>> (<<biblio.biblionumber>>).','email'); >- > 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 <a href=\"/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=RoutingListNote#jumped\">RoutingListNote</a> system preference.','70|10','Define a note to be shown on all routing lists','Textarea'), > ('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 ) %]<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_manage_rota_items && Koha.Preference('StockRotation') ) %][% IF ( stockrotationview ) %]<li class="active">[% ELSE %]<li>[% END %]<a href="/cgi-bin/koha/catalogue/stockrotation.pl?biblionumber=[% biblio_object_id %]">Rota</a> </li>[% END %] > </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 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' -%]<span>Log into the self check-in module. Note: this permission prevents the patron from using any other OPAC functionality</span> > [%- CASE 'self_checkout_module' -%]<span>Perform self checkout at the OPAC. It should be used for the patron matching the AutoSelfCheckID</span> >+ [%- CASE 'manage_rota_items' -%]<span>Add and remove items from rotas</span> >+ [%- CASE 'manage_rotas' -%]<span>Create, edit and delete rotas</span> > [%- END -%] >- [%- 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..f574b19978 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/stockrotation-toolbar.inc >@@ -0,0 +1,12 @@ >+[% 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="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 %] >+ [% IF op == 'manage_items' %] >+ <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 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 %] >+ > <div id="navmenu"> > <div id="navmenulist"> > <ul> >@@ -38,6 +40,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_manage_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/catalogue/stockrotation.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/stockrotation.tt >new file mode 100644 >index 0000000000..5d6238dff0 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/stockrotation.tt >@@ -0,0 +1,171 @@ >+[% 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' %] >+[% Asset.js("js/browser.js") %] >+<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>Callnumber</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 %] >+ [% IF item.rota.active %] >+ Active >+ [% ELSE %] >+ Inactive >+ [% END %] >+ [% IF !item.rota.active %] >+ </span> >+ [% END %] >+ [% END %] >+ </td> >+ <td> >+ [% IF item.bib_item.get_transfer %] >+ Yes >+ [% ELSE %] >+ No >+ [% END %] >+ </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"> >+ [% IF item.rota.cyclical %] >+ START >+ [% ELSE %] >+ END >+ [% 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> >+ [% IF item.stockrotationitem.indemand %] >+ Remove "In demand" >+ [% ELSE %] >+ Add "In demand" >+ [% END %] >+ </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..9dec3d93b2 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stockrotation.tt >@@ -0,0 +1,510 @@ >+[% USE Koha %] >+[% USE Branches %] >+[% USE KohaDates %] >+[% INCLUDE 'doc-head-open.inc' %] >+<title>Koha › Stock rotation</title> >+[% INCLUDE 'doc-head-close.inc' %] >+[% Asset.css("css/datatables.css") %] >+[% INCLUDE 'datatables.inc' %] >+[% Asset.js("js/pages/stockrotation.js") %] >+<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"> >+ >+ [% IF no_op_set %] >+ >+ [% INCLUDE 'stockrotation-toolbar.inc' %] >+ >+ <h2>Stock rotation</h2> >+ >+ [% IF existing_rotas.size > 0 %] >+ <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_manage_rota_items && 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 %] >+ <h2>Edit "[% rota.title %]"</h2> >+ [% ELSE %] >+ <h2>Create new rota</h2> >+ [% 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="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 error == 'invalid_form' %] >+ <div class="dialog alert"> >+ <h3>There was a problem with your form submission</h3> >+ </div> >+ [% END %] >+ >+ <h2>Manage [% rota.title %] stages</h2> >+ <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> >+ >+ <form id="stage_form" method="post" enctype="multipart/form-data" class="validated"> >+ <fieldset class="rows"> >+ <legend>Add stage</legend> >+ <ol> >+ <li> >+ <label class="required" for="branch">Library:</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="Submit"> >+ </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> >+ >+ [% IF existing_stages.size > 0 %] >+ <div id="manage_stages"> >+ <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">Library</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 %] >+ <h2>Edit "[% Branches.GetName(stage.branchcode_id) %]"</h2> >+ [% ELSE %] >+ <h2>Create new stage</h2> >+ [% 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">Library:</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 %] >+ >+ <h2>Manage [% rota.title %] items</h2> >+ >+ <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>Callnumber</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 a035ee146b..3b3cf011c7 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' %] >@@ -66,6 +68,11 @@ > [% IF ( CAN_user_tools_batch_upload_patron_images ) %] > <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_manage_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> >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..a6cd0c12b1 >--- /dev/null >+++ b/misc/cronjobs/stockrotation.pl >@@ -0,0 +1,528 @@ >+#!/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 C4::Context; >+use C4::Letters; >+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 >+--------------------\n"; >+ $body .= sprintf " >+ 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, >+ { >+ letter => { >+ title => 'Stockrotation Report', >+ content => $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 } ); >+ } >+ elsif ( $data->{actionable} ) { # Full email report >+ while ( my ( $branchcode_id, $details ) = each %{$branched} ) { >+ push @{$out}, _report_per_branch($details) >+ 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 ($branch) = @_; >+ >+ my $status = 0; >+ if ( $branch && @{ $branch->{items} } ) { >+ $status = 1; >+ } >+ >+ if ( >+ my $letter = C4::Letters::GetPreparedLetter( >+ module => 'circulation', >+ letter_code => "SR_SLIP", >+ message_transport_type => 'email', >+ substitute => $branch >+ ) >+ ) >+ { >+ return { >+ letter => $letter, >+ email_address => $branch->{email}, >+ $status >+ }; >+ } >+ return; >+} >+ >+=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_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->{status}: a boolean indicating whether the reported part is empty or not >+# - part->{email_address}: the email address to send the report to >+# - part->{no_branch_email}: a boolean indicating that we are missing a branch email >+# - part->{letter}: a GetPreparedLetter hash as returned by the C4::Letters module >+ my $report = $params->{report}; >+ my $header = shift @{$report}; >+ my $parts = $report; >+ >+ my @emails; >+ foreach my $part ( @{$parts} ) { >+ >+ if ( $part->{status} || $params->{send_all} ) { >+ >+ # We have a report to send, or we want to send even empty >+ # reports. >+ >+ # Send to branch >+ my $addressee; >+ if ( $part->{email_address} ) { >+ $addressee = $part->{email_address}; >+ } >+ elsif ( !$part->{no_branch_email} ) { >+ >+#push @emails, "***We tried to send a branch report, but we have no email address for this branch.***\n\n"; >+ $addressee = C4::Context->preference('KohaAdminEmailAddress') >+ if ( C4::Context->preference('KohaAdminEmailAddress') ); >+ } >+ >+ if ( $params->{send_email} ) { # Only email if emails requested >+ if ( defined($addressee) ) { >+ C4::Letters::EnqueueLetter( >+ { >+ letter => $part->{letter}, >+ to_address => $addressee, >+ message_transport_type => 'email', >+ } >+ ) >+ or warn >+ "can't enqueue letter $part->{letter} for $addressee"; >+ } >+ >+ # Copy to admin? >+ if ( $params->{admin_email} ) { >+ C4::Letters::EnqueueLetter( >+ { >+ letter => $part->{letter}, >+ to_address => $params->{admin_email}, >+ message_transport_type => 'email', >+ } >+ ) >+ or warn >+"can't enqueue letter $part->{letter} for $params->{admin_email}"; >+ } >+ } >+ else { >+ my $email = >+ "-------- Email message --------" . "\n\n" . "To: " >+ . defined($addressee) ? $addressee >+ : defined( $params->{admin_email} ) ? $params->{admin_email} >+ : '' . "\n" >+ . "Subject: " >+ . $part->{letter}->{title} . "\n\n" >+ . $part->{letter}->{content}; >+ push @emails, $email; >+ } >+ } >+ } >+ >+ # Emit to stdout instead of email? >+ if ( !$params->{send_email} ) { >+ >+ # The final message is the header + body of this part. >+ my $msg = $header; >+ $msg .= "No database updates have been performed.\n\n" >+ unless ( $params->{execute} ); >+ >+ # Append email reports to message >+ $msg .= join( "\n\n", @emails ); >+ printf $msg; >+ } >+} >+ >+#### Main Code >+ >+# Compile Stockrotation Report data >+my $rotas = Koha::StockRotationRotas->search(undef,{ order_by => { '-asc' => 'title' }}); >+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 92151d662d..a3287351f4 100755 >--- a/t/db_dependent/Items.t >+++ b/t/db_dependent/Items.t >@@ -830,6 +830,67 @@ subtest 'Test logging for ModItem' => sub { > $schema->resultset('ActionLog')->search()->delete(); > ModItem({ location => $location }, $bibnum, $itemnumber); > is( $schema->resultset('ActionLog')->count(), 1, 'Undefined value defaults to true, triggers logging' ); >+}; >+ >+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; > }; >diff --git a/t/db_dependent/Koha/Libraries.t b/t/db_dependent/Koha/Libraries.t >index d24ba3d6fe..ef6a10b6be 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 => 4; >+use Test::More tests => 6; > > use Koha::Library; > use Koha::Libraries; >@@ -53,6 +53,29 @@ is( $retrieved_library_1->branchname, $new_library_1->branchname, 'Find a librar > $retrieved_library_1->delete; > is( Koha::Libraries->search->count, $nb_of_libraries + 1, 'Delete should have deleted the library' ); > >+# 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..a5a064a334 >--- /dev/null >+++ b/tools/stockrotation.pl >@@ -0,0 +1,531 @@ >+#!/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', >+ flagsrequired => { >+ tools => '*', >+ stockrotation => '*', >+ }, >+ 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( >+ undef, >+ { >+ order_by => { -asc => 'title' } >+ } >+ )->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 >+ }; >+ >+ } 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}, >+ 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'); >+ >+ # 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} >+ )->description( >+ $sub_rota->{description} >+ )->store; >+ >+ } else { >+ >+ $rota = Koha::StockRotationRota->new({ >+ title => $sub_rota->{title}, >+ cyclical => $sub_rota->{cyclical}, >+ active => 0, >+ 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.18.0
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