Bug 22954: Minor markup error in OPAC messaging template
[koha.git] / misc / cronjobs / stockrotation.pl
blob083c14766a0f9142b3c107f3e1b2d1f6ab510fd4
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/;
112 use C4::Context;
113 use C4::Letters;
114 use Koha::StockRotationRotas;
116 my $admin_email = '';
117 my $branch = 0;
118 my $execute = 0;
119 my $report = 'full';
120 my $send_all = 0;
121 my $send_email = 0;
123 my $ok = GetOptions(
124 'admin-email|a=s' => \$admin_email,
125 'branchcode|b=s' => sub {
126 my ( $opt_name, $opt_value ) = @_;
127 my $branches = Koha::Libraries->search( {},
128 { order_by => { -asc => 'branchname' } } );
129 my $brnch = $branches->find($opt_value);
130 if ($brnch) {
131 $branch = $brnch;
132 return $brnch;
134 else {
135 printf("Option $opt_name should be one of (name -> code):\n");
136 while ( my $candidate = $branches->next ) {
137 printf( " %-40s -> %s\n",
138 $candidate->branchname, $candidate->branchcode );
140 exit 1;
143 'execute|x' => \$execute,
144 'report|r=s' => sub {
145 my ( $opt_name, $opt_value ) = @_;
146 if ( $opt_value eq 'full' || $opt_value eq 'email' ) {
147 $report = $opt_value;
149 else {
150 printf("Option $opt_name should be either 'email' or 'full'.\n");
151 exit 1;
154 'send-all|S' => \$send_all,
155 'send-email|s' => \$send_email,
156 'help|h|?' => sub { HelpMessage }
158 exit 1 unless ($ok);
160 $send_email++ if ($send_all); # if we send all, then we must want emails.
162 =head2 Helpers
164 =head3 execute
166 undef = execute($report);
168 Perform the database updates, within a transaction, that are reported as
169 needing to be performed by $REPORT.
171 $REPORT should be the return value of an invocation of `investigate`.
173 This procedure WILL mess with your database.
175 =cut
177 sub execute {
178 my ($data) = @_;
180 # Begin transaction
181 my $schema = Koha::Database->new->schema;
182 $schema->storage->txn_begin;
184 # Carry out db updates
185 foreach my $item ( @{ $data->{items} } ) {
186 my $reason = $item->{reason};
187 if ( $reason eq 'repatriation' ) {
188 $item->{object}->repatriate;
190 elsif ( grep { $reason eq $_ } qw/in-demand advancement initiation/ ) {
191 $item->{object}->advance;
195 # End transaction
196 $schema->storage->txn_commit;
199 =head3 report_full
201 my $full_report = report_full($report);
203 Return an arrayref containing a string containing a detailed report about the
204 current state of the stockrotation subsystem.
206 $REPORT should be the return value of `investigate`.
208 No data in the database is manipulated by this procedure.
210 =cut
212 sub report_full {
213 my ($data) = @_;
215 my $header = "";
216 my $body = "";
218 # Summary
219 $header .= "STOCKROTATION REPORT\n";
220 $header .= "--------------------\n";
221 $body .= sprintf "
222 Total number of rotas: %5u
223 Inactive rotas: %5u
224 Active rotas: %5u
225 Total number of items: %5u
226 Inactive items: %5u
227 Stationary items: %5u
228 Actionable items: %5u
229 Total items to be initiated: %5u
230 Total items to be repatriated: %5u
231 Total items to be advanced: %5u
232 Total items in demand: %5u\n\n",
233 $data->{sum_rotas}, $data->{rotas_inactive}, $data->{rotas_active},
234 $data->{sum_items}, $data->{items_inactive}, $data->{stationary},
235 $data->{actionable}, $data->{initiable}, $data->{repatriable},
236 $data->{advanceable}, $data->{indemand};
238 if ( @{ $data->{rotas} } ) { # Per Rota details
239 $body .= "ROTAS DETAIL\n";
240 $body .= "------------\n\n";
241 foreach my $rota ( @{ $data->{rotas} } ) {
242 $body .= sprintf "Details for %s [%s]:\n",
243 $rota->{name}, $rota->{id};
244 $body .= "\n Items:"; # Rota item details
245 if ( @{ $rota->{items} } ) {
246 $body .=
247 join( "", map { _print_item($_) } @{ $rota->{items} } );
249 else {
250 $body .= "\n No items to be processed for this rota.\n";
252 $body .= "\n Log:"; # Rota log details
253 if ( @{ $rota->{log} } ) {
254 $body .= join( "", map { _print_item($_) } @{ $rota->{log} } );
256 else {
257 $body .= "\n No items in log for this rota.\n\n";
261 return [
262 $header,
264 letter => {
265 title => 'Stockrotation Report',
266 content => $body # The body of the report
268 status => 1, # We have a meaningful report
269 no_branch_email => 1, # We don't expect branch email in report
274 =head3 report_email
276 my $email_report = report_email($report);
278 Returns an arrayref containing a header string, with basic report information,
279 and any number of 'per_branch' strings, containing a detailed report about the
280 current state of the stockrotation subsystem, from the perspective of those
281 individual branches.
283 $REPORT should be the return value of `investigate`, and $BRANCH should be
284 either 0 (to indicate 'all'), or a specific Koha::Library object.
286 No data in the database is manipulated by this procedure.
288 =cut
290 sub report_email {
291 my ( $data, $branch ) = @_;
293 my $out = [];
294 my $header = "";
296 # Summary
297 my $branched = $data->{branched};
298 my $flag = 0;
300 $header .= "BRANCH-BASED STOCKROTATION REPORT\n";
301 $header .= "---------------------------------\n";
302 push @{$out}, $header;
304 if ($branch) { # Branch limited report
305 push @{$out}, _report_per_branch( $branched->{ $branch->branchcode } );
307 elsif ( $data->{actionable} ) { # Full email report
308 while ( my ( $branchcode_id, $details ) = each %{$branched} ) {
309 push @{$out}, _report_per_branch($details)
310 if ( @{ $details->{items} } );
313 else {
314 push @{$out}, {
315 body => "No actionable items at any libraries.\n\n", # The body of the report
316 no_branch_email => 1, # We don't expect branch email in report
319 return $out;
322 =head3 _report_per_branch
324 my $branch_string = _report_per_branch($branch_details, $branchcode, $branchname);
326 return a string containing details about the stockrotation items and their
327 status for the branch identified by $BRANCHCODE.
329 This helper procedure is only used from within `report_email`.
331 No data in the database is manipulated by this procedure.
333 =cut
335 sub _report_per_branch {
336 my ($branch) = @_;
338 my $status = 0;
339 if ( $branch && @{ $branch->{items} } ) {
340 $status = 1;
343 if (
344 my $letter = C4::Letters::GetPreparedLetter(
345 module => 'circulation',
346 letter_code => "SR_SLIP",
347 message_transport_type => 'email',
348 substitute => $branch
352 return {
353 letter => $letter,
354 email_address => $branch->{email},
355 $status
358 return;
361 =head3 _print_item
363 my $string = _print_item($item_section);
365 Return a string containing an overview about $ITEM_SECTION.
367 This helper procedure is only used from within `report_full`.
369 No data in the database is manipulated by this procedure.
371 =cut
373 sub _print_item {
374 my ($item) = @_;
375 return sprintf "
376 Title: %s
377 Author: %s
378 Callnumber: %s
379 Location: %s
380 Barcode: %s
381 On loan?: %s
382 Status: %s
383 Current Library: %s [%s]\n\n",
384 $item->{title} || "N/A", $item->{author} || "N/A",
385 $item->{callnumber} || "N/A", $item->{location} || "N/A",
386 $item->{barcode} || "N/A", $item->{onloan} ? 'Yes' : 'No',
387 $item->{reason} || "N/A", $item->{branch}->branchname,
388 $item->{branch}->branchcode;
391 =head3 emit
393 undef = emit($params);
395 $PARAMS should be a hashref of the following format:
396 admin_email: the address to which a copy of all reports should be sent.
397 execute: the flag indicating whether we performed db updates
398 send_all: the flag indicating whether we should send even empty reports
399 send_email: the flag indicating whether we want to emit to stdout or email
400 report: the data structure returned from one of the report procedures
402 No data in the database is manipulated by this procedure.
404 The return value is unspecified: we simply emit a message as a side-effect or
405 die.
407 =cut
409 sub emit {
410 my ($params) = @_;
412 # REPORT is an arrayref of at least 2 elements:
413 # - The header for the report, which will be repeated for each part
414 # - a "part" for each report we want to emit
415 # PARTS are hashrefs:
416 # - part->{status}: a boolean indicating whether the reported part is empty or not
417 # - part->{email_address}: the email address to send the report to
418 # - part->{no_branch_email}: a boolean indicating that we are missing a branch email
419 # - part->{letter}: a GetPreparedLetter hash as returned by the C4::Letters module
420 my $report = $params->{report};
421 my $header = shift @{$report};
422 my $parts = $report;
424 my @emails;
425 foreach my $part ( @{$parts} ) {
427 if ( $part->{status} || $params->{send_all} ) {
429 # We have a report to send, or we want to send even empty
430 # reports.
432 # Send to branch
433 my $addressee;
434 if ( $part->{email_address} ) {
435 $addressee = $part->{email_address};
437 elsif ( !$part->{no_branch_email} ) {
439 #push @emails, "***We tried to send a branch report, but we have no email address for this branch.***\n\n";
440 $addressee = C4::Context->preference('KohaAdminEmailAddress')
441 if ( C4::Context->preference('KohaAdminEmailAddress') );
444 if ( $params->{send_email} ) { # Only email if emails requested
445 if ( defined($addressee) ) {
446 C4::Letters::EnqueueLetter(
448 letter => $part->{letter},
449 to_address => $addressee,
450 message_transport_type => 'email',
453 or warn
454 "can't enqueue letter $part->{letter} for $addressee";
457 # Copy to admin?
458 if ( $params->{admin_email} ) {
459 C4::Letters::EnqueueLetter(
461 letter => $part->{letter},
462 to_address => $params->{admin_email},
463 message_transport_type => 'email',
466 or warn
467 "can't enqueue letter $part->{letter} for $params->{admin_email}";
470 else {
471 my $email =
472 "-------- Email message --------" . "\n\n" . "To: "
473 . defined($addressee) ? $addressee
474 : defined( $params->{admin_email} ) ? $params->{admin_email}
475 : '' . "\n"
476 . "Subject: "
477 . $part->{letter}->{title} . "\n\n"
478 . $part->{letter}->{content};
479 push @emails, $email;
484 # Emit to stdout instead of email?
485 if ( !$params->{send_email} ) {
487 # The final message is the header + body of this part.
488 my $msg = $header;
489 $msg .= "No database updates have been performed.\n\n"
490 unless ( $params->{execute} );
492 # Append email reports to message
493 $msg .= join( "\n\n", @emails );
494 printf $msg;
498 #### Main Code
500 # Compile Stockrotation Report data
501 my $rotas = Koha::StockRotationRotas->search(undef,{ order_by => { '-asc' => 'title' }});
502 my $data = $rotas->investigate;
504 # Perform db updates if requested
505 execute($data) if ($execute);
507 # Emit Reports
508 my $out_report = {};
509 $out_report = report_email( $data, $branch ) if $report eq 'email';
510 $out_report = report_full( $data, $branch ) if $report eq 'full';
511 emit(
513 admin_email => $admin_email,
514 execute => $execute,
515 report => $out_report,
516 send_all => $send_all,
517 send_email => $send_email,
521 =head1 AUTHOR
523 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
525 =cut