Bugzilla – Attachment 70501 Details for
Bug 11897
Stock Rotation for Koha
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 11897: Add stockrotation cron script.
Bug-11897-Add-stockrotation-cron-script.patch (text/plain), 20.32 KB, created by
Alex Sassmannshausen
on 2018-01-15 18:18:31 UTC
(
hide
)
Description:
Bug 11897: Add stockrotation cron script.
Filename:
MIME Type:
Creator:
Alex Sassmannshausen
Created:
2018-01-15 18:18:31 UTC
Size:
20.32 KB
patch
obsolete
>From b1daa1996aac5e1ed3ca5414737a2a0069fed3df Mon Sep 17 00:00:00 2001 >From: Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> >Date: Fri, 21 Oct 2016 09:55:16 +0200 >Subject: [PATCH] Bug 11897: Add stockrotation cron script. > >* misc/cronjobs/stockrotation.pl: New file. > >Signed-off-by: Kathleen Milne <kathleen.milne@cne-siar.gov.uk> >--- > 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 <alex.sassmannshausen@ptfs-europe.com> >+ >+=cut >-- >2.13.4
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 11897
:
25887
|
58207
|
58208
|
58209
|
58210
|
58211
|
58212
|
58213
|
58214
|
58215
|
58216
|
58217
|
58218
|
58219
|
58280
|
58281
|
58687
|
58692
|
58693
|
58694
|
58695
|
58696
|
60175
|
60715
|
61825
|
61826
|
61827
|
61828
|
61829
|
61830
|
61831
|
61832
|
61833
|
61834
|
61835
|
61836
|
61837
|
61838
|
61839
|
61885
|
62730
|
62750
|
62751
|
62752
|
62753
|
62754
|
62755
|
62756
|
62757
|
62758
|
62759
|
62760
|
62761
|
62762
|
62763
|
62764
|
62765
|
62766
|
62767
|
63063
|
63064
|
63065
|
63066
|
63067
|
63068
|
63069
|
63070
|
63071
|
63072
|
63073
|
63074
|
63075
|
63076
|
63077
|
63078
|
63079
|
63080
|
63114
|
64850
|
64851
|
65715
|
65716
|
65717
|
65718
|
65719
|
65720
|
65721
|
65722
|
65723
|
65724
|
65725
|
65726
|
65727
|
65728
|
65729
|
65730
|
65731
|
65732
|
65733
|
65734
|
65735
|
65736
|
65737
|
65738
|
65739
|
65740
|
65741
|
65742
|
65743
|
65744
|
65745
|
65746
|
65747
|
65748
|
65749
|
65750
|
65751
|
65752
|
65753
|
65754
|
65755
|
65756
|
65757
|
65758
|
66381
|
70496
|
70497
|
70498
|
70499
|
70500
|
70501
|
70502
|
70503
|
70504
|
70505
|
70506
|
70507
|
70508
|
70509
|
70510
|
70511
|
70512
|
70513
|
70514
|
70515
|
70516
|
70517
|
70518
|
70519
|
70520
|
70521
|
70522
|
70523
|
70524
|
72124
|
72125
|
72126
|
72127
|
72128
|
74003
|
74004
|
74005
|
74006
|
74007
|
74008
|
74010
|
74011
|
74012
|
74013
|
74014
|
74015
|
74016
|
74017
|
74197
|
74198
|
74199
|
74200
|
74201
|
74202
|
74203
|
74204
|
74205
|
74206
|
74319
|
74320
|
74360
|
74459
|
74460
|
74461
|
74462
|
74463
|
74464
|
74465
|
74466
|
74467
|
74468
|
74469
|
74470
|
75409
|
75410
|
75411
|
75412
|
75413
|
75414
|
75415
|
75416
|
75417
|
75418
|
75419
|
75420
|
76121
|
76122
|
76123
|
76124
|
76125
|
76126
|
76127
|
76128
|
76129
|
76130
|
76131
|
76132
|
76133
|
76134
|
76135
|
76144
|
76706
|
76707
|
76708
|
76709
|
76710
|
77623
|
77624
|
77625
|
77626
|
78387
|
78388
|
78389
|
78390
|
78391
|
78392
|
78393
|
78394
|
78404
|
78405
|
78406
|
78407
|
78408
|
79040
|
79041
|
79042
|
79043
|
79044
|
79738
|
79739
|
79740
|
79741
|
79742
|
79746
|
79747
|
79748
|
79749
|
79750
|
79964
|
79965
|
79966
|
79967
|
79968
|
79969
|
79970
|
79971
|
79972
|
79973
|
79974
|
79975
|
80068
|
80087
|
80088
|
80089
|
80090
|
80091
|
80092
|
80093
|
80094
|
80095
|
80096
|
80097
|
80098
|
80099
|
80100
|
80118
|
80260
|
80261
|
80262
|
80263
|
80264
|
80265
|
80266
|
80267
|
80268
|
80269
|
80270
|
80271
|
80272
|
80273
|
80274
|
80275
|
80289
|
80298
|
80299
|
80300