Bug 22723: Correct syntax error on confess call in Koha/MetadataRecord/Authority.pm
[koha.git] / misc / cronjobs / stockrotation.pl
blob69ae97e38f24631eb5ae94568c0ba420ac086c84
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 my $branches = Koha::Libraries->search( {},
130 { order_by => { -asc => 'branchname' } } );
131 my $brnch = $branches->find($opt_value);
132 if ($brnch) {
133 $branch = $brnch;
134 return $brnch;
136 else {
137 printf("Option $opt_name should be one of (name -> code):\n");
138 while ( my $candidate = $branches->next ) {
139 printf( " %-40s -> %s\n",
140 $candidate->branchname, $candidate->branchcode );
142 exit 1;
145 'execute|x' => \$execute,
146 'report|r=s' => sub {
147 my ( $opt_name, $opt_value ) = @_;
148 if ( $opt_value eq 'full' || $opt_value eq 'email' ) {
149 $report = $opt_value;
151 else {
152 printf("Option $opt_name should be either 'email' or 'full'.\n");
153 exit 1;
156 'send-all|S' => \$send_all,
157 'send-email|s' => \$send_email,
158 'help|h|?' => sub { HelpMessage }
160 exit 1 unless ($ok);
162 $send_email++ if ($send_all); # if we send all, then we must want emails.
164 =head2 Helpers
166 =head3 execute
168 undef = execute($report);
170 Perform the database updates, within a transaction, that are reported as
171 needing to be performed by $REPORT.
173 $REPORT should be the return value of an invocation of `investigate`.
175 This procedure WILL mess with your database.
177 =cut
179 sub execute {
180 my ($data) = @_;
182 # Begin transaction
183 my $schema = Koha::Database->new->schema;
184 $schema->storage->txn_begin;
186 # Carry out db updates
187 foreach my $item ( @{ $data->{items} } ) {
188 my $reason = $item->{reason};
189 if ( $reason eq 'repatriation' ) {
190 $item->{object}->repatriate;
192 elsif ( grep { $reason eq $_ } qw/in-demand advancement initiation/ ) {
193 $item->{object}->advance;
197 # End transaction
198 $schema->storage->txn_commit;
201 =head3 report_full
203 my $full_report = report_full($report);
205 Return an arrayref containing a string containing a detailed report about the
206 current state of the stockrotation subsystem.
208 $REPORT should be the return value of `investigate`.
210 No data in the database is manipulated by this procedure.
212 =cut
214 sub report_full {
215 my ($data) = @_;
217 my $header = "";
218 my $body = "";
220 # Summary
221 $header .= "STOCKROTATION REPORT\n";
222 $header .= "--------------------\n";
223 $body .= sprintf "
224 Total number of rotas: %5u
225 Inactive rotas: %5u
226 Active rotas: %5u
227 Total number of items: %5u
228 Inactive items: %5u
229 Stationary items: %5u
230 Actionable items: %5u
231 Total items to be initiated: %5u
232 Total items to be repatriated: %5u
233 Total items to be advanced: %5u
234 Total items in demand: %5u\n\n",
235 $data->{sum_rotas}, $data->{rotas_inactive}, $data->{rotas_active},
236 $data->{sum_items}, $data->{items_inactive}, $data->{stationary},
237 $data->{actionable}, $data->{initiable}, $data->{repatriable},
238 $data->{advanceable}, $data->{indemand};
240 if ( @{ $data->{rotas} } ) { # Per Rota details
241 $body .= "ROTAS DETAIL\n";
242 $body .= "------------\n\n";
243 foreach my $rota ( @{ $data->{rotas} } ) {
244 $body .= sprintf "Details for %s [%s]:\n",
245 $rota->{name}, $rota->{id};
246 $body .= "\n Items:"; # Rota item details
247 if ( @{ $rota->{items} } ) {
248 $body .=
249 join( "", map { _print_item($_) } @{ $rota->{items} } );
251 else {
252 $body .= "\n No items to be processed for this rota.\n";
254 $body .= "\n Log:"; # Rota log details
255 if ( @{ $rota->{log} } ) {
256 $body .= join( "", map { _print_item($_) } @{ $rota->{log} } );
258 else {
259 $body .= "\n No items in log for this rota.\n\n";
263 return [
264 $header,
266 letter => {
267 title => 'Stockrotation Report',
268 content => $body # The body of the report
270 status => 1, # We have a meaningful report
271 no_branch_email => 1, # We don't expect branch email in report
276 =head3 report_email
278 my $email_report = report_email($report);
280 Returns an arrayref containing a header string, with basic report information,
281 and any number of 'per_branch' strings, containing a detailed report about the
282 current state of the stockrotation subsystem, from the perspective of those
283 individual branches.
285 $REPORT should be the return value of `investigate`, and $BRANCH should be
286 either 0 (to indicate 'all'), or a specific Koha::Library object.
288 No data in the database is manipulated by this procedure.
290 =cut
292 sub report_email {
293 my ( $data, $branch ) = @_;
295 my $out = [];
296 my $header = "";
298 # Summary
299 my $branched = $data->{branched};
300 my $flag = 0;
302 $header .= "BRANCH-BASED STOCKROTATION REPORT\n";
303 $header .= "---------------------------------\n";
304 push @{$out}, $header;
306 if ($branch) { # Branch limited report
307 push @{$out}, _report_per_branch( $branched->{ $branch->branchcode } );
309 elsif ( $data->{actionable} ) { # Full email report
310 while ( my ( $branchcode_id, $details ) = each %{$branched} ) {
311 push @{$out}, _report_per_branch($details)
312 if ( @{ $details->{items} } );
315 else {
316 push @{$out}, {
317 body => "No actionable items at any libraries.\n\n", # The body of the report
318 no_branch_email => 1, # We don't expect branch email in report
321 return $out;
324 =head3 _report_per_branch
326 my $branch_string = _report_per_branch($branch_details, $branchcode, $branchname);
328 return a string containing details about the stockrotation items and their
329 status for the branch identified by $BRANCHCODE.
331 This helper procedure is only used from within `report_email`.
333 No data in the database is manipulated by this procedure.
335 =cut
337 sub _report_per_branch {
338 my ($branch) = @_;
340 my $status = 0;
341 if ( $branch && @{ $branch->{items} } ) {
342 $status = 1;
345 if (
346 my $letter = C4::Letters::GetPreparedLetter(
347 module => 'circulation',
348 letter_code => "SR_SLIP",
349 message_transport_type => 'email',
350 substitute => $branch
354 return {
355 letter => $letter,
356 email_address => $branch->{email},
357 $status
360 return;
363 =head3 _print_item
365 my $string = _print_item($item_section);
367 Return a string containing an overview about $ITEM_SECTION.
369 This helper procedure is only used from within `report_full`.
371 No data in the database is manipulated by this procedure.
373 =cut
375 sub _print_item {
376 my ($item) = @_;
377 return sprintf "
378 Title: %s
379 Author: %s
380 Callnumber: %s
381 Location: %s
382 Barcode: %s
383 On loan?: %s
384 Status: %s
385 Current Library: %s [%s]\n\n",
386 $item->{title} || "N/A", $item->{author} || "N/A",
387 $item->{callnumber} || "N/A", $item->{location} || "N/A",
388 $item->{barcode} || "N/A", $item->{onloan} ? 'Yes' : 'No',
389 $item->{reason} || "N/A", $item->{branch}->branchname,
390 $item->{branch}->branchcode;
393 =head3 emit
395 undef = emit($params);
397 $PARAMS should be a hashref of the following format:
398 admin_email: the address to which a copy of all reports should be sent.
399 execute: the flag indicating whether we performed db updates
400 send_all: the flag indicating whether we should send even empty reports
401 send_email: the flag indicating whether we want to emit to stdout or email
402 report: the data structure returned from one of the report procedures
404 No data in the database is manipulated by this procedure.
406 The return value is unspecified: we simply emit a message as a side-effect or
407 die.
409 =cut
411 sub emit {
412 my ($params) = @_;
414 # REPORT is an arrayref of at least 2 elements:
415 # - The header for the report, which will be repeated for each part
416 # - a "part" for each report we want to emit
417 # PARTS are hashrefs:
418 # - part->{status}: a boolean indicating whether the reported part is empty or not
419 # - part->{email_address}: the email address to send the report to
420 # - part->{no_branch_email}: a boolean indicating that we are missing a branch email
421 # - part->{letter}: a GetPreparedLetter hash as returned by the C4::Letters module
422 my $report = $params->{report};
423 my $header = shift @{$report};
424 my $parts = $report;
426 my @emails;
427 foreach my $part ( @{$parts} ) {
429 if ( $part->{status} || $params->{send_all} ) {
431 # We have a report to send, or we want to send even empty
432 # reports.
434 # Send to branch
435 my $addressee;
436 if ( $part->{email_address} ) {
437 $addressee = $part->{email_address};
439 elsif ( !$part->{no_branch_email} ) {
441 #push @emails, "***We tried to send a branch report, but we have no email address for this branch.***\n\n";
442 $addressee = C4::Context->preference('KohaAdminEmailAddress')
443 if ( C4::Context->preference('KohaAdminEmailAddress') );
446 if ( $params->{send_email} ) { # Only email if emails requested
447 if ( defined($addressee) ) {
448 C4::Letters::EnqueueLetter(
450 letter => $part->{letter},
451 to_address => $addressee,
452 message_transport_type => 'email',
455 or warn
456 "can't enqueue letter $part->{letter} for $addressee";
459 # Copy to admin?
460 if ( $params->{admin_email} ) {
461 C4::Letters::EnqueueLetter(
463 letter => $part->{letter},
464 to_address => $params->{admin_email},
465 message_transport_type => 'email',
468 or warn
469 "can't enqueue letter $part->{letter} for $params->{admin_email}";
472 else {
473 my $email =
474 "-------- Email message --------" . "\n\n" . "To: "
475 . defined($addressee) ? $addressee
476 : defined( $params->{admin_email} ) ? $params->{admin_email}
477 : '' . "\n"
478 . "Subject: "
479 . $part->{letter}->{title} . "\n\n"
480 . $part->{letter}->{content};
481 push @emails, $email;
486 # Emit to stdout instead of email?
487 if ( !$params->{send_email} ) {
489 # The final message is the header + body of this part.
490 my $msg = $header;
491 $msg .= "No database updates have been performed.\n\n"
492 unless ( $params->{execute} );
494 # Append email reports to message
495 $msg .= join( "\n\n", @emails );
496 printf $msg;
500 #### Main Code
502 # Compile Stockrotation Report data
503 my $rotas = Koha::StockRotationRotas->search(undef,{ order_by => { '-asc' => 'title' }});
504 my $data = $rotas->investigate;
506 # Perform db updates if requested
507 execute($data) if ($execute);
509 # Emit Reports
510 my $out_report = {};
511 $out_report = report_email( $data, $branch ) if $report eq 'email';
512 $out_report = report_full( $data, $branch ) if $report eq 'full';
513 emit(
515 admin_email => $admin_email,
516 execute => $execute,
517 report => $out_report,
518 send_all => $send_all,
519 send_email => $send_email,
523 =head1 AUTHOR
525 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
527 =cut