From 33081f6cd3ad72cebf23b3354714f1288a0db621 Mon Sep 17 00:00:00 2001 From: Alex Sassmannshausen Date: Fri, 21 Oct 2016 09:55:16 +0200 Subject: [PATCH] Bug 11897: Add stockrotation cron script. * misc/cronjobs/stockrotation.pl: New file. --- misc/cronjobs/stockrotation.pl | 581 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 581 insertions(+) create mode 100755 misc/cronjobs/stockrotation.pl diff --git a/misc/cronjobs/stockrotation.pl b/misc/cronjobs/stockrotation.pl new file mode 100755 index 0000000000..8a021507ff --- /dev/null +++ b/misc/cronjobs/stockrotation.pl @@ -0,0 +1,581 @@ +#!/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 + -OR- + stockrotation.pl -h + +=cut + +use Modern::Perl; +use Getopt::Long qw/HelpMessage :config gnu_getopt/; +use Mail::Sendmail; +use C4::Context; +use Koha::Email; +use Koha::Stockrotationrotas; + +my $admin_email = ''; +my $branch = 0; +my $execute = 0; +my $report = 'full'; +my $send_all = 0; +my $send_email = 0; + +my $ok = GetOptions( + 'admin-email|a=s' => \$admin_email, + 'branchcode|b=s' => sub { + my ($opt_name, $opt_value) = @_; + my $branches = Koha::Libraries->search( + {}, { order_by => { -asc => 'branchname'} } + ); + my $brnch = $branches->find($opt_value); + if ( $brnch ) { + $branch = $brnch; + return $brnch; + } else { + printf("Option $opt_name should be one of (name -> code):\n"); + while ( my $candidate = $branches->next ) { + printf( + " %-40s -> %s\n", $candidate->branchname, + $candidate->branchcode + ); + } + exit 1 + } + }, + 'execute|x' => \$execute, + 'report|r=s' => sub { + my ($opt_name, $opt_value) = @_; + if ( $opt_value eq 'full' || $opt_value eq 'email' ) { + $report = $opt_value; + } else { + printf("Option $opt_name should be either 'email' or 'full'.\n"); + exit 1; + } + }, + 'send-all|S' => \$send_all, + 'send-email|s' => \$send_email, + 'help|h|?' => sub { HelpMessage } +); +exit 1 unless ( $ok ); + +$send_email++ if ( $send_all ); # if we send all, then we must want emails. + +=head2 Helpers + +=head3 investigate + + my $report = investigate($rotas); + +Return a datastructure which contains a wealth of information about the +current state of the stockrotation submodule, and which can be used by +`report_full`, `report_email` and `execute` to either emit reports or perform +database updates. + +$ROTAS should be an iterable set of Koha::Stockrotationrotas. + +No data in the database is manipulated by this procedure. + +=cut + +sub investigate { + my ( $rotas ) = @_; + # Initiate return variables. + my $all_relevant_items = []; + my $all_relevant_rotas = []; + my $branched = {}; + my $rotas_active = 0; + my $rotas_inactive = 0; + my $sum_items = 0; + my $items_inactive = 0; + my $stationary = 0; + my $advanceable = 0; + my $expulseable = 0; + my $repatriable = 0; + my $initiable = 0; + my $actionable = 0; + + # Each 'rota' entry in `rotas` will contain an `items` section with items + # to be moved, and a `logs` section, with items that will not be moved, + # with a reason for not moving it. + while ( my $rota = $rotas->next ) { + if ( $rota->active ) { + $rotas_active++; + my $items = []; + my $log = []; + my $stages = $rota->stockrotationstages; + while ( my $stage = $stages->next) { + my $old_stage = $stage; + my $new_stage = $stage->next_sibling; + my $duration = $stage->duration; + my $branch = $old_stage->_result->branchcode; + my $branchcode = $branch->branchcode; + $branched->{$branchcode} = { + code => $branchcode, + name => $branch->branchname, + email => $branch->branchemail, + phone => $branch->branchphone, + items => [], + }; + my $stage_items = $stage->stockrotationitems; + while ( my $item = $stage_items->next ) { + $sum_items++; + # Generate item details + my $item_report = { + title => $item->itemnumber->_result->biblioitem + ->biblionumber->title, + author => $item->itemnumber->_result->biblioitem + ->biblionumber->author, + callnumber => $item->itemnumber->itemcallnumber, + location => $item->itemnumber->location, + onloan => $item->itemnumber->onloan, + barcode => $item->itemnumber->barcode, + itemnumber => $item->itemnumber_id, + old_stage => $old_stage, + new_stage => $new_stage, + object => $item, + }; + + # Test whether we need to perform an action on item. + my $flag = 0; + if ( $item->needs_initiating ) { + $flag++; + $initiable++; + $actionable++; + $item_report->{reason} = 'initiation'; + } elsif ( $item->needs_repatriating ) { + $flag++; + $repatriable++; + $actionable++; + $item_report->{reason} = 'repatriation'; + } elsif ( $item->needs_advancing ) { + $flag++; + $actionable++; + if ( !$new_stage ) { + $expulseable++; + } else { + $advanceable++; + } + $item_report->{reason} = + ( $item->indemand ) ? 'in-demand' : 'advancement'; + } + + if ( $flag ) { # Item will be moved. + push @{$items}, $item_report; + push @{$all_relevant_items}, $item_report; + push @{$branched->{$branchcode}->{items}}, $item_report; + } else { # Item not ready to be moved. + $stationary++; + $item_report->{reason} = 'not-ready'; + push @{$log}, $item_report; + } + } + } + # Add our rota report to the full report. + push @{$all_relevant_rotas}, { + name => $rota->title, + id => $rota->rota_id, + items => $items, + log => $log, + }; + } else { # Rota is not active. + $rotas_inactive++; + $sum_items += $rota->stockrotationitems->count; + $items_inactive += $rota->stockrotationitems->count; + } + } + + return { + sum_rotas => $rotas->count, + rotas_inactive => $rotas_inactive, + rotas_active => $rotas_active, + sum_items => $sum_items, + items_inactive => $items_inactive, + stationary => $stationary, + actionable => $actionable, + initiable => $initiable, + repatriable => $repatriable, + advanceable => $advanceable, + expulseable => $expulseable, + rotas => $all_relevant_rotas, + items => $all_relevant_items, + branched => $branched, + } +} + +=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 'initiation' ) { + $item->{object}->initiate; + } elsif ( $reason eq 'repatriation' ) { + $item->{object}->repatriate; + } elsif ( $reason eq 'in-demand' || $reason eq 'advancement' ) { + $item->{object}->advance; + } + } + + # End transaction + $schema->storage->txn_commit; +} + +=head3 report_full + + my $full_report = report_full($report); + +Return an arrayref containing a string containing a detailed report about the +current state of the stockrotation subsystem. + +$REPORT should be the return value of `investigate`. + +No data in the database is manipulated by this procedure. + +=cut + +sub report_full { + my ( $data ) = @_; + + my $header = ""; + my $body = ""; + # Summary + $header .= sprintf " +STOCKROTATION REPORT +-------------------- + Total number of rotas: %5u + Inactive rotas: %5u + Active rotas: %5u + Total number of items: %5u + Inactive items: %5u + Stationary items: %5u + Actionable items: %5u + Total items to be initiated: %5u + Total items to be repatriated: %5u + Total items to be advanced: %5u + Total items to be expulsed: %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->{expulseable}; + + if (@{$data->{rotas}}) { # Per Rota details + $body .= sprintf "ROTAS DETAIL\n------------\n\n"; + foreach my $rota (@{$data->{rotas}}) { + $body .= sprintf "Details for %s [%s]:\n", + $rota->{name}, $rota->{id}; + $body .= sprintf "\n Items:"; # Rota item details + if (@{$rota->{items}}) { + $body .= join("", map { _print_item($_) } @{$rota->{items}}); + } else { + $body .= sprintf + "\n No items to be processed for this rota.\n"; + } + $body .= sprintf "\n Log:"; # Rota log details + if (@{$rota->{log}}) { + $body .= join("", map { _print_item($_) } @{$rota->{log}}); + } else { + $body .= sprintf "\n No items in log for this rota.\n\n"; + } + } + } + return [ $header, { + body => $body, # The body of the report + status => 1, # We have a meaningful report + no_branch_email => 1, # We don't expect branch email in report + } ]; +} + +=head3 report_email + + my $email_report = report_email($report); + +Returns an arrayref containing a header string, with basic report information, +and any number of 'per_branch' strings, containing a detailed report about the +current state of the stockrotation subsystem, from the perspective of those +individual branches. + +$REPORT should be the return value of `investigate`, and $BRANCH should be +either 0 (to indicate 'all'), or a specific Koha::Library object. + +No data in the database is manipulated by this procedure. + +=cut + +sub report_email { + my ( $data, $branch ) = @_; + + my $out = []; + my $header = ""; + # Summary + my $branched = $data->{branched}; + my $flag = 0; + + $header .= sprintf " +BRANCH-BASED STOCKROTATION REPORT +---------------------------------\n"; + push @{$out}, $header; + + if ( $branch ) { # Branch limited report + push @{$out}, _report_per_branch( + $branched->{$branch->branchcode}, $branch->branchcode, + $branch->branchname + ); + } elsif ( $data->{actionable} ) { # Full email report + while ( my ($branchcode_id, $details) = each %{$branched} ) { + push @{$out}, _report_per_branch( + $details, $details->{code}, $details->{name} + ) if ( @{$details->{items}} ); + } + } else { + push @{$out}, { + body => sprintf " +No actionable items at any libraries.\n\n", # The body of the report + no_branch_email => 1, # We don't expect branch email in report + }; + } + return $out; +} + +=head3 _report_per_branch + + my $branch_string = _report_per_branch($branch_details, $branchcode, $branchname); + +return a string containing details about the stockrotation items and their +status for the branch identified by $BRANCHCODE. + +This helper procedure is only used from within `report_email`. + +No data in the database is manipulated by this procedure. + +=cut + +sub _report_per_branch { + my ( $per_branch, $branchcode, $branchname ) = @_; + + my $out = ""; + my $status = 0; + + $out .= sprintf "\nStockrotation report for '%s' [%s]:\n", + $branchname || "N/A", $branchcode || "N/A"; + if ( $per_branch && @{$per_branch->{items}} ) { + $out .= sprintf " + Email: %s + Phone: %s + Items:", + $per_branch->{email} || "N/A", $per_branch->{phone} || "N/A"; + $status++; + } else { + $out .= sprintf "No items to be processed for this branch.\n\n"; + } + $out .= join("", map { _print_item($_) } @{$per_branch->{items}}); + return { + body => $out, # The body of the report + email_address => $per_branch->{email}, # The branch email address + status => $status, # We may have empty report... + }; +} + +=head3 _print_item + + my $string = _print_item($item_section); + +Return a string containing an overview about $ITEM_SECTION. + +This helper procedure is only used from within `report_email` and +`report_full`. + +No data in the database is manipulated by this procedure. + +=cut + +sub _print_item { + my ( $item ) = @_; + return sprintf " + Title: %s + Author: %s + Callnumber: %s + Location: %s + Barcode: %s + On loan?: %s + Status: %s\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"; +} + +=head3 emit + + undef = emit($params); + +$PARAMS should be a hashref of the following format: + admin_email: the address to which a copy of all reports should be sent. + execute: the flag indicating whether we performed db updates + send_all: the flag indicating whether we should send even empty reports + send_email: the flag indicating whether we want to emit to stdout or email + report: the data structure returned from one of the report procedures + +No data in the database is manipulated by this procedure. + +The return value is unspecified: we simply emit a message as a side-effect or +die. + +=cut + +sub emit { + my ( $params ) = @_; + + # REPORT is an arrayref of at least 2 elements: + # - The header for the report, which will be repeated for each part + # - a "part" for each report we want to emit + # PARTS are hashrefs: + # - part->{body}: the body of the report + # - part->{email_address}: the email address to send the report to + my $report = $params->{report}; + my $header = shift @{$report}; + my $parts = $report; + + foreach my $part (@{$parts}) { + # The final message is the header + body of this part. + my $msg = $header . $part->{body}; + $msg .= "No database updates have been performed.\n\n" + unless ( $params->{execute} ); + + if ( $params->{send_email} ) { # Only email if emails requested + if ( $part->{status} || $params->{send_all} ) { + # We have a report to send, or we want to send even empty + # reports. + my $message = Koha::Email->new; + + # Collate 'to' addresses + my @to = (); + if ( $part->{email_address} ) { + push @to, $part->{email_address}; + } elsif ( !$part->{no_branch_email} ) { + $msg = "***We tried to send a branch report, but we have no email address for this branch.***\n\n" . $msg; + push @to, C4::Context->preference('KohaAdminEmailAddress') + if ( C4::Context->preference('KohaAdminEmailAddress') ); + } + push @to, $params->{admin_email} if $params->{admin_email}; + + # Create email data. + my %mail = $message->create_message_headers( + { + to => join("; ", @to), + subject => "Stockrotation Email Report", + message => Encode::encode("utf8", $msg), + contenttype => 'text/plain; charset="utf8"', + } + ); + + # Send or die. + die($Mail::Sendmail::error) + unless Mail::Sendmail::sendmail(%mail); + } + + } else { # Else emit to stdout. + printf $msg; + } + } +} + +#### Main Code + +# Compile Stockrotation Report data +my $rotas = Koha::Stockrotationrotas->search; +my $data = investigate($rotas); + +# 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 + +=cut -- 2.12.2