Bug 22566: (QA follow-up) Fix pod complaint
[koha.git] / misc / cronjobs / stockrotation.pl
blobce67c38e964843278d75ac700b1215a20fc66803
1 #!/usr/bin/perl
3 # Copyright 2016 PTFS Europe
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 3 of the License, or (at your option) any later
10 # version.
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 =head1 NAME
22 stockrotation.pl
24 =head1 SYNOPSIS
26 --[a]dmin-email An address to which email reports should also be sent
27 --[b]ranchcode Select branch to report on for 'email' reports (default: all)
28 --e[x]ecute Actually perform stockrotation housekeeping
29 --[r]eport Select either 'full' or 'email'
30 --[S]end-all Send email reports even if the report body is empty
31 --[s]end-email Send reports by email
32 --[h]elp Display this help message
34 Cron script implementing scheduled stockrotation functionality.
36 By default this script merely reports on the current status of the
37 stockrotation subsystem. In order to actually place items in transit, the
38 script must be run with the `execute` argument.
40 `report` allows you to select the type of report that will be emitted. It's
41 set to 'full' by default. If the `email` report is selected, you can use the
42 `branchcode` parameter to specify which branch's report you would like to see.
43 The default is 'all'.
45 `admin-email` is an additional email address to which we will send all email
46 reports in addition to sending them to branch email addresses.
48 `send-email` will cause the script to send reports by email, and `send-all`
49 will cause even reports with an empty body to be sent.
51 =head1 DESCRIPTION
53 This script is used to move items from one stockrotationstage to the next,
54 if they are elible for processing.
56 it should be run from cron like:
58 stockrotation.pl --report email --send-email --execute
60 Prior to that you can run the script from the command line without the
61 --execute and --send-email parameters to see what reports the script would
62 generate in 'production' mode. This is immensely useful for testing, or for
63 getting to understand how the stockrotation module works: you can set up
64 different scenarios, and then "query" the system on what it would do.
66 Normally you would want to run this script once per day, probably around
67 midnight-ish to move any stockrotationitems along their rotas and to generate
68 the email reports for branch libraries.
70 Each library will receive a report with "items of interest" for them for
71 today's rota checks. Each item there will be an item that should, according
72 to Koha, be located on the shelves of that branch, and which should be picked
73 up and checked in. The item will either:
74 - have been placed in transit to their new stage library;
75 - have been placed in transit to be returned to their current stage library;
76 - have just been added to a rota and will already be at the correct library;
78 In the last case the item will be checked in and no message will pop up. In
79 the other cases a message will pop up requesting the item be posted to their
80 new branch.
82 =head2 What does the --execute flag do?
84 To understand this, you will need to know a little bit about the design of
85 this script and the stockrotation modules.
87 This script operates in 3 phases: first it walks the graph of rotas, stages
88 and items. For each active rota, it investigates the items in each stage and
89 determines whether action is required. It does not perform any actions, it
90 just "sieves" all items on active rotas into "actionable" and "non-actionable"
91 baskets. We can use these baskets to perform actions against the items, or to
92 generate reports.
94 During the second phase this script then loops through the actionable baskets,
95 and performs the relevant action (initiate, repatriate, advance) on each item.
97 Finally, during the third phase we revisit the original baskets and we compile
98 reports (for instance per branch email reports).
100 When the script is run without the "--execute" flag, we perform phase 1, skip
101 phase 2 and move straight onto phase 3.
103 With the "--execute" flag we also perform the database operations.
105 So with or without the flag, the report will look the same (except for the "No
106 database updates have been performed.").
108 =cut
110 use Modern::Perl;
111 use Getopt::Long qw/HelpMessage :config gnu_getopt/;
113 use Koha::Script -cron;
114 use C4::Context;
115 use C4::Letters;
116 use Koha::StockRotationRotas;
118 my $admin_email = '';
119 my $branch = 0;
120 my $execute = 0;
121 my $report = 'full';
122 my $send_all = 0;
123 my $send_email = 0;
125 my $ok = GetOptions(
126 'admin-email|a=s' => \$admin_email,
127 'branchcode|b=s' => sub {
128 my ( $opt_name, $opt_value ) = @_;
129 if ( $opt_value eq 'all' ) {
130 $branch = 0;
132 else {
133 my $branches = Koha::Libraries->search( {},
134 { order_by => { -asc => 'branchname' } } );
135 my $brnch = $branches->find($opt_value);
136 if ($brnch) {
137 $branch = $brnch;
138 return $brnch;
140 else {
141 printf("Option $opt_name should be one of (name -> code):\n");
142 while ( my $candidate = $branches->next ) {
143 printf( " %-40s -> %s\n",
144 $candidate->branchname, $candidate->branchcode );
146 exit 1;
150 'execute|x' => \$execute,
151 'report|r=s' => sub {
152 my ( $opt_name, $opt_value ) = @_;
153 if ( $opt_value eq 'full' || $opt_value eq 'email' ) {
154 $report = $opt_value;
156 else {
157 printf("Option $opt_name should be either 'email' or 'full'.\n");
158 exit 1;
161 'send-all|S' => \$send_all,
162 'send-email|s' => \$send_email,
163 'help|h|?' => sub { HelpMessage }
165 exit 1 unless ($ok);
167 $send_email++ if ($send_all); # if we send all, then we must want emails.
169 if ( $send_email && !$admin_email && ($report eq 'full')) {
170 printf("Sending the full report by email requires --admin-email.\n");
171 exit 1;
174 =head2 Helpers
176 =head3 execute
178 undef = execute($report);
180 Perform the database updates, within a transaction, that are reported as
181 needing to be performed by $REPORT.
183 $REPORT should be the return value of an invocation of `investigate`.
185 This procedure WILL mess with your database.
187 =cut
189 sub execute {
190 my ($data) = @_;
192 # Begin transaction
193 my $schema = Koha::Database->new->schema;
194 $schema->storage->txn_begin;
196 # Carry out db updates
197 foreach my $item ( @{ $data->{items} } ) {
198 my $reason = $item->{reason};
199 if ( $reason eq 'repatriation' ) {
200 $item->{object}->repatriate;
202 elsif ( grep { $reason eq $_ } qw/in-demand advancement initiation/ ) {
203 $item->{object}->advance;
207 # End transaction
208 $schema->storage->txn_commit;
211 =head3 report_full
213 my $full_report = report_full($report);
215 Return an arrayref containing a string containing a detailed report about the
216 current state of the stockrotation subsystem.
218 $REPORT should be the return value of `investigate`.
220 No data in the database is manipulated by this procedure.
222 =cut
224 sub report_full {
225 my ($data) = @_;
227 my $header = "";
228 my $body = "";
230 # Summary
231 $header .= "STOCKROTATION REPORT\n";
232 $header .= "--------------------\n";
233 $body .= sprintf "
234 Total number of rotas: %5u
235 Inactive rotas: %5u
236 Active rotas: %5u
237 Total number of items: %5u
238 Inactive items: %5u
239 Stationary items: %5u
240 Actionable items: %5u
241 Total items to be initiated: %5u
242 Total items to be repatriated: %5u
243 Total items to be advanced: %5u
244 Total items in demand: %5u\n\n",
245 $data->{sum_rotas}, $data->{rotas_inactive}, $data->{rotas_active},
246 $data->{sum_items}, $data->{items_inactive}, $data->{stationary},
247 $data->{actionable}, $data->{initiable}, $data->{repatriable},
248 $data->{advanceable}, $data->{indemand};
250 if ( @{ $data->{rotas} } ) { # Per Rota details
251 $body .= "ROTAS DETAIL\n";
252 $body .= "------------\n\n";
253 foreach my $rota ( @{ $data->{rotas} } ) {
254 $body .= sprintf "Details for %s [%s]:\n",
255 $rota->{name}, $rota->{id};
256 $body .= "\n Items:"; # Rota item details
257 if ( @{ $rota->{items} } ) {
258 $body .=
259 join( "", map { _print_item($_) } @{ $rota->{items} } );
261 else {
262 $body .= "\n No items to be processed for this rota.\n";
264 $body .= "\n Log:"; # Rota log details
265 if ( @{ $rota->{log} } ) {
266 $body .= join( "", map { _print_item($_) } @{ $rota->{log} } );
268 else {
269 $body .= "\n No items in log for this rota.\n\n";
273 return [
274 $header,
276 letter => {
277 title => 'Stockrotation Report',
278 content => $body # The body of the report
280 status => 1, # We have a meaningful report
281 no_branch_email => 1, # We don't expect branch email in report
286 =head3 report_by_branch
288 my $email_report = report_by_branch($report, [$branch]);
290 Returns an arrayref containing a header string, with basic report information,
291 and any number of 'per_branch' strings, containing a detailed report about the
292 current state of the stockrotation subsystem, from the perspective of those
293 individual branches.
295 =over 2
297 =item $report should be the return value of `investigate`
299 =item $branch is optional and should be either 0 (to indicate 'all'), or a specific Koha::Library object.
301 =back
303 No data in the database is manipulated by this procedure.
305 =cut
307 sub report_by_branch {
308 my ( $data, $branch ) = @_;
310 my $out = [];
311 my $header = "";
313 # Summary
314 my $branched = $data->{branched};
315 my $flag = 0;
317 $header .= "BRANCH-BASED STOCKROTATION REPORT\n";
318 $header .= "---------------------------------\n";
319 push @{$out}, $header;
321 if ($branch) { # Branch limited report
322 push @{$out}, _report_per_branch( $branched->{ $branch->branchcode } );
324 elsif ( $data->{actionable} ) { # Full email report
325 while ( my ( $branchcode_id, $details ) = each %{$branched} ) {
326 push @{$out}, _report_per_branch($details)
327 if ( @{ $details->{items} } );
330 else {
331 push @{$out}, {
332 body => "No actionable items at any libraries.\n\n", # The body of the report
333 no_branch_email => 1, # We don't expect branch email in report
336 return $out;
339 =head3 _report_per_branch
341 my $branch_string = _report_per_branch($branch_details);
343 return a string containing details about the stockrotation items and their
344 status for the branch identified by $BRANCHCODE.
346 This helper procedure is only used from within `report_by_branch`.
348 No data in the database is manipulated by this procedure.
350 =cut
352 sub _report_per_branch {
353 my ($branch) = @_;
355 my $status = 0;
356 if ( $branch && @{ $branch->{items} } ) {
357 $status = 1;
360 if (
361 my $letter = C4::Letters::GetPreparedLetter(
362 module => 'circulation',
363 letter_code => "SR_SLIP",
364 branchcode => $branch->{code},
365 message_transport_type => 'email',
366 substitute => { branch => $branch }
370 return {
371 letter => $letter,
372 email_address => $branch->{email},
373 status => $status
376 return;
379 =head3 _print_item
381 my $string = _print_item($item_section);
383 Return a string containing an overview about $ITEM_SECTION.
385 This helper procedure is only used from within `report_full`.
387 No data in the database is manipulated by this procedure.
389 =cut
391 sub _print_item {
392 my ($item) = @_;
393 return sprintf "
394 Title: %s
395 Author: %s
396 Callnumber: %s
397 Location: %s
398 Barcode: %s
399 On loan?: %s
400 Status: %s
401 Current Library: %s [%s]\n\n",
402 $item->{title} || "N/A", $item->{author} || "N/A",
403 $item->{callnumber} || "N/A", $item->{location} || "N/A",
404 $item->{barcode} || "N/A", $item->{onloan} ? 'Yes' : 'No',
405 $item->{reason} || "N/A", $item->{branch}->branchname,
406 $item->{branch}->branchcode;
409 =head3 emit
411 undef = emit($params);
413 $PARAMS should be a hashref of the following format:
414 admin_email: the address to which a copy of all reports should be sent.
415 execute: the flag indicating whether we performed db updates
416 send_all: the flag indicating whether we should send even empty reports
417 send_email: the flag indicating whether we want to emit to stdout or email
418 report: the data structure returned from one of the report procedures
420 No data in the database is manipulated by this procedure.
422 The return value is unspecified: we simply emit a message as a side-effect or
423 die.
425 =cut
427 sub emit {
428 my ($params) = @_;
430 # REPORT is an arrayref of at least 2 elements:
431 # - The header for the report, which will be repeated for each part
432 # - a "part" for each report we want to emit
433 # PARTS are hashrefs:
434 # - part->{status}: a boolean indicating whether the reported part is empty or not
435 # - part->{email_address}: the email address to send the report to
436 # - part->{no_branch_email}: a boolean indicating that we are missing a branch email
437 # - part->{letter}: a GetPreparedLetter hash as returned by the C4::Letters module
438 my $report = $params->{report};
439 my $header = shift @{$report};
440 my $parts = $report;
442 my @emails;
443 foreach my $part ( @{$parts} ) {
445 if ( $part->{status} || $params->{send_all} ) {
447 # We have a report to send, or we want to send even empty
448 # reports.
450 # Select email address to send to
451 my $addressee;
452 if ( $part->{email_address} ) {
453 $addressee = $part->{email_address};
455 elsif ( !$part->{no_branch_email} ) {
456 $addressee = C4::Context->preference('KohaAdminEmailAddress')
457 if ( C4::Context->preference('KohaAdminEmailAddress') );
460 if ( $params->{send_email} ) { # Only email if emails requested
461 if ( defined($addressee) ) {
462 C4::Letters::EnqueueLetter(
464 letter => $part->{letter},
465 to_address => $addressee,
466 message_transport_type => 'email',
469 or warn
470 "can't enqueue letter $part->{letter} for $addressee";
473 # Copy to admin?
474 if ( $params->{admin_email} ) {
475 C4::Letters::EnqueueLetter(
477 letter => $part->{letter},
478 to_address => $params->{admin_email},
479 message_transport_type => 'email',
482 or warn
483 "can't enqueue letter $part->{letter} for $params->{admin_email}";
486 else {
487 my $email =
488 "-------- Email message --------" . "\n\n";
489 $email .= "To: $addressee\n";
490 $email .= "Cc: " . $params->{admin_email} . "\n"
491 if ( $params->{admin_email} );
492 $email .= "Subject: "
493 . $part->{letter}->{title} . "\n\n"
494 . $part->{letter}->{content};
495 push @emails, $email;
500 # Emit to stdout instead of email?
501 if ( !$params->{send_email} ) {
503 # The final message is the header + body of this part.
504 my $msg = $header;
505 $msg .= "No database updates have been performed.\n\n"
506 unless ( $params->{execute} );
508 # Append email reports to message
509 $msg .= join( "\n\n", @emails );
510 printf $msg;
514 #### Main Code
516 # Compile Stockrotation Report data
517 my $rotas = Koha::StockRotationRotas->search(undef,{ order_by => { '-asc' => 'title' }});
518 my $data = $rotas->investigate;
520 # Perform db updates if requested
521 execute($data) if ($execute);
523 # Emit Reports
524 my $out_report = {};
525 $out_report = report_by_branch( $data, $branch ) if $report eq 'email';
526 $out_report = report_full( $data, $branch ) if $report eq 'full';
527 emit(
529 admin_email => $admin_email,
530 execute => $execute,
531 report => $out_report,
532 send_all => $send_all,
533 send_email => $send_email,
537 =head1 AUTHOR
539 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
541 =cut