From 3e75980630f803c49d874efbbb74a5e0a114c2a9 Mon Sep 17 00:00:00 2001 From: Andrew Isherwood Date: Wed, 14 Dec 2016 10:14:38 +0000 Subject: [PATCH] Bug 11897: Interface for rotas from Tools menu * tools/stockrotation.pl: New file * koha-tmpl/intranet-tmpl/prog/en/modules/tools/stockrotation.tt: New file * koha-tmpl/intranet-tmpl/prog/en/includes/stockrotation-toolbar.inc: --- .../prog/en/includes/stockrotation-toolbar.inc | 13 + .../prog/en/modules/tools/stockrotation.tt | 482 +++++++++++++++++++ tools/stockrotation.pl | 519 +++++++++++++++++++++ 3 files changed, 1014 insertions(+) create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/stockrotation-toolbar.inc create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/tools/stockrotation.tt create mode 100755 tools/stockrotation.pl diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/stockrotation-toolbar.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/stockrotation-toolbar.inc new file mode 100644 index 0000000..5bc4a8b --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/stockrotation-toolbar.inc @@ -0,0 +1,13 @@ +[% USE Koha %] +
+ [% IF no_op_set %] + New rota + [% END %] + [% IF op == 'manage_stages' %] + New stage + [% END %] + [% IF op == 'manage_items' %] + New rota + Edit rota + [% END %] +
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stockrotation.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stockrotation.tt new file mode 100644 index 0000000..acf9b89 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stockrotation.tt @@ -0,0 +1,482 @@ +[% USE Koha %] +[% USE Branches %] +[% USE KohaDates %] +[% INCLUDE 'doc-head-open.inc' %] +Koha › Stock rotation +[% INCLUDE 'doc-head-close.inc' %] + +[% INCLUDE 'datatables.inc' %] + + + + + +[% INCLUDE 'header.inc' %] +[% INCLUDE 'patron-search.inc' %] + + + +
+
+
+
+ +

Stock rotation

+ + [% IF no_op_set %] + + [% INCLUDE 'stockrotation-toolbar.inc' %] + + [% IF existing_rotas.size > 0 %] +

Manage existing rotas

+ + + + + + + + + + + [% FOREACH rota IN existing_rotas %] + + + + + + + + + [% END %] + +
NameCyclicalActiveDescriptionNumber of items 
[% rota.title %][% rota.cyclical ? 'Yes' : 'No'%][% rota.active ? 'Yes' : 'No'%][% rota.description %][% rota.stockrotationitems.count %] + + + Edit + +
+ + +
+ + + [% IF !rota.active %] + Activate + [% ELSE %] + Deactivate + [% END %] + +
+ [% END %] + + [% ELSIF (op == 'create_edit_rota') %] + + [% IF rota.rota_id %] +

Edit "[% rota.title %]"

+ [% ELSE %] +

Create new rota

+ [% END %] + + [% IF error == 'invalid_form' %] +
+

There was a problem with your form submission

+
+ [% END %] + +
+
+
    +
  1. + + + Required +
  2. +
  3. + + +
  4. +
  5. + + +
  6. +
  7. + + +
  8. +
+
+
+ + Cancel +
+ [% IF rota.rota_id %] + + [% END %] + +
+ + [% ELSIF (op == 'manage_stages') %] + + [% INCLUDE 'stockrotation-toolbar.inc' %] + + [% IF existing_stages.size > 0 %] +
+

Manage stages

+
+ + + + + + +
+
+ Stages can be re-ordered by using the handle to drag and drop them to their new position +
+
+ Branch + Duration (days) +
+
    + [% FOREACH stage IN existing_stages %] +
  • + + [% IF existing_stages.size > 1 %] + + [% END %] + [% Branches.GetName(stage.branchcode_id) %] + + [% stage.duration %] + + + Edit + + + Delete + + +
  • + [% END %] +
+
+ [% END %] + +

Return to rotas

+ + [% ELSIF (op == 'create_edit_stage') %] + + [% IF stage.id %] +

Edit "[% Branches.GetName(stage.branchcode_id) %]"

+ [% ELSE %] +

Create new stage

+ [% END %] + + [% IF error == 'invalid_form' %] +
+

There was a problem with your form submission

+
+ [% END %] + +
+
+
    +
  1. + + + Required +
  2. +
  3. + + + Required +
  4. +
+
+
+ + Cancel +
+ + + +
+ [% ELSIF (op == 'confirm_remove_from_rota') %] + +
+

Are you sure you wish to remove this item from it's rota

+

+ Yes + No +

+
+ [% ELSIF (op == 'confirm_delete_stage') %] + +
+

Are you sure you want to delete this stage?

+ [% IF stage.stockrotationitems.count > 0 %] +

This stage contains the following item(s):

+
    + [% FOREACH item IN stage.stockrotationitems %] +
  • [% item.itemnumber.biblio.title %] (Barcode: [% item.itemnumber.barcode %])
  • + [% END %] +
+ [% END %] +

+ Yes + No +

+
+ [% ELSIF (op == 'manage_items') %] + + [% INCLUDE 'stockrotation-toolbar.inc' %] + + [% IF error %] +
+ [% IF error == "item_not_found" %] +

The item was not found

+ [% ELSIF error == "already_on_rota" %] +

This item is already on this rota

+ [% END %] +
+ [% END %] + +
+
+
+ Add item to "[% rota.title %]" +
    +
  1. + + +
  2. +
+
+
+ Use a barcode file +
    +
  1. + + +
  2. +
+
+
+ +
+ + +
+
+ + [% IF items.count > 0 %] +

Manage items assigned to "[% rota.title %]"

+ + + + + + + + + + + + [% FOREACH item IN items %] + + + + + + + + + + [% END %] + +
BarcodeTitleAuthorShelfmarkIn transitStages & duration in days
(current stage highlighted)
 
[% item.itemnumber.barcode %][% item.itemnumber.biblio.title %][% item.itemnumber.biblio.author %][% item.itemnumber.itemcallnumber %][% item.itemnumber.get_transfer ? 'Yes' : 'No' %] + [% FOREACH this_stage IN stages %] + [% IF this_stage.stage_id == item.stage.stage_id %] + + [% ELSE %] + + [% END %] + [% Branches.GetName(this_stage.branchcode_id) %] ([% this_stage.duration %]) + + » + [% END %] + [% IF stages.size > 0 %] + [% rota.cyclical ? 'START' : 'END' %] + [% END %] + + [% in_transit = item.itemnumber.get_transfer %] + [% IF !in_transit && stages.size > 1 %] + + [% ELSE %] + + [% END %] + + Move to next stage + + [% IF !in_transit %] + + [% ELSE %] + + [% END %] + + [% item.indemand ? 'Remove "In demand"' : 'Add "In demand"' %] + + [% IF !in_transit %] + + [% ELSE %] + + [% END %] + + Remove from rota + +
+ [% END %] + +

Return to rotas

+ + [% ELSIF op == 'add_items_to_rota' %] + + +
+

Add items to rota report

+
+
+ [% IF barcode_status.ok.size > 0 %] +

Items added to rota:

+
    + [% FOREACH item_ok IN barcode_status.ok %] +
  • [% item_ok.biblio.title %]
  • + [% END %] +
+ [% END %] + [% IF barcode_status.on_this.size > 0 %] +

Items already on this rota:

+
    + [% FOREACH item_on_this IN barcode_status.on_this %] +
  • [% item_on_this.biblio.title %]
  • + [% END %] +
+ [% END %] + [% IF barcode_status.not_found.size > 0 %] +

Barcodes not found:

+
    + [% FOREACH barcode_not_found IN barcode_status.not_found %] +
  • [% barcode_not_found %]
  • + [% END %] +
+ [% END %] + [% IF barcode_status.on_other.size > 0 %] +

Items found on other rotas:

+
    + [% FOREACH item_on_other IN barcode_status.on_other %] +
  • [% item_on_other.biblio.title %]
  • + [% END %] +
+ [% END %] +
+ [% IF barcode_status.on_other.size > 0 %] +
+
+ Select items to move to this rota: + [% FOREACH item_on_other IN barcode_status.on_other %] +
  • [% item_on_other.biblio.title %] (Currently on "[% item_on_other.stockrotationitem.stage.rota.title %]")
  • + [% END %] + +
    +
    + +
    + + +
    + [% END %] +

    Return to rota

    + + [% END %] +
    +
    +
    + [% INCLUDE 'tools-menu.inc' %] +
    +
    +
    +[% INCLUDE 'intranet-bottom.inc' %] diff --git a/tools/stockrotation.pl b/tools/stockrotation.pl new file mode 100755 index 0000000..0074270 --- /dev/null +++ b/tools/stockrotation.pl @@ -0,0 +1,519 @@ +#!/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::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; + +my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => 'tools/stockrotation.tt', + query => $input, + type => 'intranet', + authnotrequired => 0 + } +); + +# Grab all passed data +# 'our' since Plack changes the scoping +# of 'my' +our %params = $input->Vars(); + +my $op = $params{op}; + +if (!defined $op) { + + # No operation is supplied, we're just displaying the list of rotas + my $rotas = Koha::Stockrotationrotas->search->as_list; + + $template->param( + existing_rotas => $rotas, + no_op_set => 1 + ); + +} elsif ($op eq 'create_edit_rota') { + + # Edit an existing rota or define a new one + my $rota_id = $params{rota_id}; + + my $rota = {}; + + if (!defined $rota_id) { + + # No ID supplied, we're creating a new rota + # Create a shell rota hashref + $rota = { + cyclical => 1, + active => 0 + }; + + } else { + + # ID supplied, we're editing an existing rota + $rota = Koha::Stockrotationrotas->find($rota_id); + + } + + $template->param( + rota => $rota, + op => $op + ); + +} elsif ($op eq 'toggle_rota') { + + # Find and update the active status of the rota + my $rota = Koha::Stockrotationrotas->find($params{rota_id}); + + my $new_active = ($rota->active == 1) ? 0 : 1; + + $rota->active($new_active)->store; + + # Return to rotas page + print $input->redirect('stockrotation.pl'); + +} elsif ($op eq 'process_rota') { + + # Get a hashref of the submitted rota data + my $rota = get_rota_from_form(); + + if (!process_rota($rota)) { + + # The submitted rota was invalid + $template->param( + error => 'invalid_form', + rota => $rota, + op => 'create_edit_rota' + ); + + } else { + + # All was well, return to the rotas list + print $input->redirect('stockrotation.pl'); + + } + +} elsif ($op eq 'manage_stages') { + + my $rota = Koha::Stockrotationrotas->find($params{rota_id}); + + $template->param( + rota => $rota, + branches => get_branches(), + existing_stages => get_stages($rota), + rota_id => $params{rota_id}, + op => $op + ); + +} elsif ($op eq 'create_edit_stage') { + + # Edit an existing stage or define a new one + my $stage_id = $params{stage_id}; + + my $rota_id = $params{rota_id}; + + if (!defined $stage_id) { + + # No ID supplied, we're creating a new stage + $template->param( + branches => get_branches(), + stage => {}, + rota_id => $rota_id, + op => $op + ); + + } else { + + # ID supplied, we're editing an existing stage + my $stage = Koha::Stockrotationstages->find($stage_id); + + $template->param( + branches => get_branches(), + stage => $stage, + rota_id => $stage->rota->rota_id, + op => $op + ); + + } + +} elsif ($op eq 'confirm_remove_from_rota') { + + # Get the stage we're deleting + $template->param( + op => $op, + rota_id => $params{rota_id}, + stage_id => $params{stage_id}, + item_id => $params{item_id} + ); + +} elsif ($op eq 'confirm_delete_stage') { + + # Get the stage we're deleting + my $stage = Koha::Stockrotationstages->find($params{stage_id}); + + $template->param( + op => $op, + stage => $stage + ); + +} elsif ($op eq 'delete_stage') { + + # Get the stage we're deleting + my $stage = Koha::Stockrotationstages->find($params{stage_id}); + + # Get the ID of the rota with which this stage is associated + # (so we can return to the "Manage stages" page after deletion) + my $rota_id = $stage->rota->rota_id; + + $stage->delete; + + # Return to the stages list + print $input->redirect("?op=manage_stages&rota_id=$rota_id"); + +} elsif ($op eq 'process_stage') { + + # Get a hashref of the submitted stage data + my $stage = get_stage_from_form(); + + # The rota we're managing + my $rota_id = $params{rota_id}; + + if (!process_stage($stage, $rota_id)) { + + # The submitted stage was invalid + # Get all branches + my $branches = get_branches(); + + $template->param( + error => 'invalid_form', + all_branches => $branches, + stage => $stage, + rota_id => $rota_id, + op => 'create_edit_stage' + ); + + } else { + + # All was well, return to the stages list + print $input->redirect("?op=manage_stages&rota_id=$rota_id"); + + } + +} elsif ($op eq 'manage_items') { + + my $rota = Koha::Stockrotationrotas->find($params{rota_id}); + + # Get all items on this rota, for each prefetch their + # stage and biblio objects + my $items = Koha::Stockrotationitems->search( + { 'stage.rota_id' => $params{rota_id} }, + { + prefetch => { + stage => { + 'stockrotationitems' => { + 'itemnumber' => 'biblionumber' + } + } + } + } + ); + + $template->param( + rota_id => $params{rota_id}, + error => $params{error}, + items => $items, + branches => get_branches(), + stages => get_stages($rota), + rota => $rota, + op => $op + ); + +} elsif ($op eq 'move_to_next_stage') { + + move_to_next_stage($params{item_id}, $params{stage_id}); + + # Return to the items list + print $input->redirect("?op=manage_items&rota_id=" . $params{rota_id}); + +} elsif ($op eq 'toggle_in_demand') { + + # Toggle the item's in_demand + toggle_indemand($params{item_id}, $params{stage_id}); + + # Return to the items list + print $input->redirect("?op=manage_items&rota_id=".$params{rota_id}); + +} elsif ($op eq 'remove_item_from_stage') { + + # Remove the item from the stage + remove_from_stage($params{item_id}, $params{stage_id}); + + # Return to the items list + print $input->redirect("?op=manage_items&rota_id=".$params{rota_id}); + +} elsif ($op eq 'add_items_to_rota') { + + # The item's barcode, + # which we may or may not have been passed + my $barcode = $params{barcode}; + + # The rota we're adding the item to + my $rota_id = $params{rota_id}; + + # The uploaded file filehandle, + # which we may or may not have been passed + my $barcode_file = $input->upload("barcodefile"); + + # We need to create an array of one or more barcodes to + # insert + my @barcodes = (); + + # If the barcode input box was populated, use it + push @barcodes, $barcode if $barcode; + + # Only parse the uploaded file if necessary + if ($barcode_file) { + + # Call binmode on the filehandle as we want to set a + # UTF-8 layer on it + binmode($barcode_file, ":encoding(UTF-8)"); + # Parse the file into an array of barcodes + while (my $barcode = <$barcode_file>) { + $barcode =~ s/\r/\n/g; + $barcode =~ s/\n+/\n/g; + my @data = split(/\n/, $barcode); + push @barcodes, @data; + } + + } + + # A hashref to hold the status of each barcode + my $barcode_status = { + ok => [], + on_other => [], + on_this => [], + not_found => [] + }; + + # If we have something to work with, do it + get_barcodes_status($rota_id, \@barcodes, $barcode_status) if (@barcodes); + + # Now we know the status of each barcode, add those that + # need it + if (scalar @{$barcode_status->{ok}} > 0) { + + add_items_to_rota($rota_id, $barcode_status->{ok}); + + } + # If we were only passed one barcode and it was successfully + # added, redirect back to ourselves, we don't want to display + # a report, redirect also if we were passed no barcodes + if ( + scalar @barcodes == 0 || + (scalar @barcodes == 1 && scalar @{$barcode_status->{ok}} == 1) + ) { + + print $input->redirect("?op=manage_items&rota_id=$rota_id"); + + } else { + + # Report on the outcome + $template->param( + barcode_status => $barcode_status, + rota_id => $rota_id, + op => $op + ); + + } + +} elsif ($op eq 'move_items_to_rota') { + + # The barcodes of the items we're moving + my @move = $input->param('move_item'); + + foreach my $item(@move) { + + # The item we're moving + my $item = Koha::Items->find($item); + + # Move it to the new rota + $item->add_to_rota($params{rota_id}); + + } + + # Return to the items list + print $input->redirect("?op=manage_items&rota_id=".$params{rota_id}); + +} + +output_html_with_http_headers $input, $cookie, $template->output; + +sub get_rota_from_form { + + return { + id => $params{id}, + title => $params{title}, + cyclical => $params{cyclical}, + active => $params{active}, + description => $params{description} + }; +} + +sub get_stage_from_form { + + return { + stage_id => $params{stage_id}, + branchcode => $params{branchcode}, + duration => $params{duration} + }; +} + +sub process_rota { + + my $sub_rota = shift; + + # Fields we require + my @required = ('title','cyclical','active'); + + # Count of the number of required fields we have + my $valid = 0; + + # Ensure we have everything we require + foreach my $req(@required) { + + if (exists $sub_rota->{$req}) { + + chomp(my $value = $sub_rota->{$req}); + if (length $value > 0) { + $valid++; + } + + } + + } + + # If we don't have everything we need + return 0 if $valid != scalar @required; + + # Passed validation + # Find the rota we're updating + my $rota = Koha::Stockrotationrotas->find($sub_rota->{id}); + + if ($rota) { + + $rota->title( + $sub_rota->{title} + )->cyclical( + $sub_rota->{cyclical} + )->active( + $sub_rota->{active} + )->description( + $sub_rota->{description} + )->store; + + } else { + + $rota = Koha::Stockrotationrota->new({ + title => $sub_rota->{title}, + cyclical => $sub_rota->{cyclical}, + active => $sub_rota->{active}, + description => $sub_rota->{description} + })->store; + + } + + return 1; +} + +sub process_stage { + + my ($sub_stage, $rota_id) = @_; + + # Fields we require + my @required = ('branchcode','duration'); + + # Count of the number of required fields we have + my $valid = 0; + + # Ensure we have everything we require + foreach my $req(@required) { + + if (exists $sub_stage->{$req}) { + + chomp(my $value = $sub_stage->{$req}); + if (length $value > 0) { + $valid++; + } + + } + + } + + # If we don't have everything we need + return 0 if $valid != scalar @required; + + # Passed validation + # Find the stage we're updating + my $stage = Koha::Stockrotationstages->find($sub_stage->{stage_id}); + + if ($stage) { + + # Updating an existing stage + $stage->branchcode_id( + $sub_stage->{branchcode} + )->duration( + $sub_stage->{duration} + )->store; + + } else { + + # Creating a new stage + $stage = Koha::Stockrotationstage->new({ + branchcode_id => $sub_stage->{branchcode}, + rota_id => $rota_id, + duration => $sub_stage->{duration} + })->store; + + } + + return 1; +} + +=head1 AUTHOR + +Andrew Isherwood + +=cut -- 2.1.4