use Koha::Item::Transfers;
use Koha::Patrons;
use Koha::Libraries;
+use Koha::StockRotationItem;
+use Koha::StockRotationRotas;
use base qw(Koha::Object);
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
use C4::Context;
use Koha::Database;
+use Koha::StockRotationStages;
use base qw(Koha::Object);
=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();
--- /dev/null
+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;
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
},
"/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"
}
}
--- /dev/null
+{
+ "/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"
+ }
+ }
+ }
+ }
+}
--- /dev/null
+#!/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
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 {
[% 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>
[%# 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 -%]
--- /dev/null
+[% 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>
+[% USE Koha %]
+
<div id="navmenu">
<div id="navmenulist">
<ul>
[% 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>
--- /dev/null
+[% 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' %]
--- /dev/null
+[% 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' %]
+[% USE Koha %]
+
[% INCLUDE 'doc-head-open.inc' %]
<title>Koha › Tools</title>
[% INCLUDE 'doc-head-close.inc' %]
<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>
--- /dev/null
+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');
+ })
+ }
+ });
+});
--- /dev/null
+#!/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
$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;
};
use Modern::Perl;
-use Test::More tests => 4;
+use Test::More tests => 6;
use Koha::Library;
use Koha::Libraries;
$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 {
--- /dev/null
+#!/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;
--- /dev/null
+#!/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;
--- /dev/null
+#!/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;
--- /dev/null
+#!/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;
--- /dev/null
+#!/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