Bug 22566: Rename 'report_email' to 'report_by_branch'
[koha.git] / misc / cronjobs / stockrotation.pl
1 #!/usr/bin/perl
2
3 # Copyright 2016 PTFS Europe
4 #
5 # This file is part of Koha.
6 #
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.
11 #
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.
15 #
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.
19
20 =head1 NAME
21
22 stockrotation.pl
23
24 =head1 SYNOPSIS
25
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
33
34 Cron script implementing scheduled stockrotation functionality.
35
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.
39
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'.
44
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.
47
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.
50
51 =head1 DESCRIPTION
52
53 This script is used to move items from one stockrotationstage to the next,
54 if they are elible for processing.
55
56 it should be run from cron like:
57
58    stockrotation.pl --report email --send-email --execute
59
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.
65
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.
69
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;
77
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.
81
82 =head2 What does the --execute flag do?
83
84 To understand this, you will need to know a little bit about the design of
85 this script and the stockrotation modules.
86
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.
93
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.
96
97 Finally, during the third phase we revisit the original baskets and we compile
98 reports (for instance per branch email reports).
99
100 When the script is run without the "--execute" flag, we perform phase 1, skip
101 phase 2 and move straight onto phase 3.
102
103 With the "--execute" flag we also perform the database operations.
104
105 So with or without the flag, the report will look the same (except for the "No
106 database updates have been performed.").
107
108 =cut
109
110 use Modern::Perl;
111 use Getopt::Long qw/HelpMessage :config gnu_getopt/;
112
113 use Koha::Script -cron;
114 use C4::Context;
115 use C4::Letters;
116 use Koha::StockRotationRotas;
117
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;
124
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;
131         }
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;
139             }
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 );
145                 }
146                 exit 1;
147             }
148         }
149     },
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;
155         }
156         else {
157             printf("Option $opt_name should be either 'email' or 'full'.\n");
158             exit 1;
159         }
160     },
161     'send-all|S'   => \$send_all,
162     'send-email|s' => \$send_email,
163     'help|h|?'     => sub { HelpMessage }
164 );
165 exit 1 unless ($ok);
166
167 $send_email++ if ($send_all);    # if we send all, then we must want emails.
168
169 =head2 Helpers
170
171 =head3 execute
172
173   undef = execute($report);
174
175 Perform the database updates, within a transaction, that are reported as
176 needing to be performed by $REPORT.
177
178 $REPORT should be the return value of an invocation of `investigate`.
179
180 This procedure WILL mess with your database.
181
182 =cut
183
184 sub execute {
185     my ($data) = @_;
186
187     # Begin transaction
188     my $schema = Koha::Database->new->schema;
189     $schema->storage->txn_begin;
190
191     # Carry out db updates
192     foreach my $item ( @{ $data->{items} } ) {
193         my $reason = $item->{reason};
194         if ( $reason eq 'repatriation' ) {
195             $item->{object}->repatriate;
196         }
197         elsif ( grep { $reason eq $_ } qw/in-demand advancement initiation/ ) {
198             $item->{object}->advance;
199         }
200     }
201
202     # End transaction
203     $schema->storage->txn_commit;
204 }
205
206 =head3 report_full
207
208   my $full_report = report_full($report);
209
210 Return an arrayref containing a string containing a detailed report about the
211 current state of the stockrotation subsystem.
212
213 $REPORT should be the return value of `investigate`.
214
215 No data in the database is manipulated by this procedure.
216
217 =cut
218
219 sub report_full {
220     my ($data) = @_;
221
222     my $header = "";
223     my $body   = "";
224
225     # Summary
226     $header .= "STOCKROTATION REPORT\n";
227     $header .= "--------------------\n";
228     $body   .= sprintf "
229   Total number of rotas:         %5u
230     Inactive rotas:              %5u
231     Active rotas:                %5u
232   Total number of items:         %5u
233     Inactive items:              %5u
234     Stationary items:            %5u
235     Actionable items:            %5u
236   Total items to be initiated:   %5u
237   Total items to be repatriated: %5u
238   Total items to be advanced:    %5u
239   Total items in demand:         %5u\n\n",
240       $data->{sum_rotas},  $data->{rotas_inactive}, $data->{rotas_active},
241       $data->{sum_items},  $data->{items_inactive}, $data->{stationary},
242       $data->{actionable}, $data->{initiable},      $data->{repatriable},
243       $data->{advanceable}, $data->{indemand};
244
245     if ( @{ $data->{rotas} } ) {    # Per Rota details
246         $body .= "ROTAS DETAIL\n";
247         $body .= "------------\n\n";
248         foreach my $rota ( @{ $data->{rotas} } ) {
249             $body .= sprintf "Details for %s [%s]:\n",
250               $rota->{name}, $rota->{id};
251             $body .= "\n  Items:";    # Rota item details
252             if ( @{ $rota->{items} } ) {
253                 $body .=
254                   join( "", map { _print_item($_) } @{ $rota->{items} } );
255             }
256             else {
257                 $body .= "\n    No items to be processed for this rota.\n";
258             }
259             $body .= "\n  Log:";      # Rota log details
260             if ( @{ $rota->{log} } ) {
261                 $body .= join( "", map { _print_item($_) } @{ $rota->{log} } );
262             }
263             else {
264                 $body .= "\n    No items in log for this rota.\n\n";
265             }
266         }
267     }
268     return [
269         $header,
270         {
271             letter => {
272                 title   => 'Stockrotation Report',
273                 content => $body                     # The body of the report
274             },
275             status          => 1,    # We have a meaningful report
276             no_branch_email => 1,    # We don't expect branch email in report
277         }
278     ];
279 }
280
281 =head3 report_by_branch
282
283   my $email_report = report_by_branch($report, [$branch]);
284
285 Returns an arrayref containing a header string, with basic report information,
286 and any number of 'per_branch' strings, containing a detailed report about the
287 current state of the stockrotation subsystem, from the perspective of those
288 individual branches.
289
290 =over 2
291
292 =item $report should be the return value of `investigate`
293 =item $branch is optional and should be either 0 (to indicate 'all'), or a specific Koha::Library object.
294
295 =back
296
297 No data in the database is manipulated by this procedure.
298
299 =cut
300
301 sub report_by_branch {
302     my ( $data, $branch ) = @_;
303
304     my $out    = [];
305     my $header = "";
306
307     # Summary
308     my $branched = $data->{branched};
309     my $flag     = 0;
310
311     $header .= "BRANCH-BASED STOCKROTATION REPORT\n";
312     $header .= "---------------------------------\n";
313     push @{$out}, $header;
314
315     if ($branch) {    # Branch limited report
316         push @{$out}, _report_per_branch( $branched->{ $branch->branchcode } );
317     }
318     elsif ( $data->{actionable} ) {    # Full email report
319         while ( my ( $branchcode_id, $details ) = each %{$branched} ) {
320             push @{$out}, _report_per_branch($details)
321               if ( @{ $details->{items} } );
322         }
323     }
324     else {
325         push @{$out}, {
326             body => "No actionable items at any libraries.\n\n",    # The body of the report
327             no_branch_email => 1,    # We don't expect branch email in report
328         };
329     }
330     return $out;
331 }
332
333 =head3 _report_per_branch
334
335   my $branch_string = _report_per_branch($branch_details);
336
337 return a string containing details about the stockrotation items and their
338 status for the branch identified by $BRANCHCODE.
339
340 This helper procedure is only used from within `report_by_branch`.
341
342 No data in the database is manipulated by this procedure.
343
344 =cut
345
346 sub _report_per_branch {
347     my ($branch) = @_;
348
349     my $status = 0;
350     if ( $branch && @{ $branch->{items} } ) {
351         $status = 1;
352     }
353
354     if (
355         my $letter = C4::Letters::GetPreparedLetter(
356             module                 => 'circulation',
357             letter_code            => "SR_SLIP",
358             branchcode             => $branch->{code},
359             message_transport_type => 'email',
360             substitute             => $branch
361         )
362       )
363     {
364         return {
365             letter        => $letter,
366             email_address => $branch->{email},
367             status        => $status
368         };
369     }
370     return;
371 }
372
373 =head3 _print_item
374
375   my $string = _print_item($item_section);
376
377 Return a string containing an overview about $ITEM_SECTION.
378
379 This helper procedure is only used from within `report_full`.
380
381 No data in the database is manipulated by this procedure.
382
383 =cut
384
385 sub _print_item {
386     my ($item) = @_;
387     return sprintf "
388     Title:           %s
389     Author:          %s
390     Callnumber:      %s
391     Location:        %s
392     Barcode:         %s
393     On loan?:        %s
394     Status:          %s
395     Current Library: %s [%s]\n\n",
396       $item->{title}      || "N/A", $item->{author}   || "N/A",
397       $item->{callnumber} || "N/A", $item->{location} || "N/A",
398       $item->{barcode} || "N/A", $item->{onloan} ? 'Yes' : 'No',
399       $item->{reason} || "N/A", $item->{branch}->branchname,
400       $item->{branch}->branchcode;
401 }
402
403 =head3 emit
404
405   undef = emit($params);
406
407 $PARAMS should be a hashref of the following format:
408   admin_email: the address to which a copy of all reports should be sent.
409   execute: the flag indicating whether we performed db updates
410   send_all: the flag indicating whether we should send even empty reports
411   send_email: the flag indicating whether we want to emit to stdout or email
412   report: the data structure returned from one of the report procedures
413
414 No data in the database is manipulated by this procedure.
415
416 The return value is unspecified: we simply emit a message as a side-effect or
417 die.
418
419 =cut
420
421 sub emit {
422     my ($params) = @_;
423
424 # REPORT is an arrayref of at least 2 elements:
425 #   - The header for the report, which will be repeated for each part
426 #   - a "part" for each report we want to emit
427 # PARTS are hashrefs:
428 #   - part->{status}: a boolean indicating whether the reported part is empty or not
429 #   - part->{email_address}: the email address to send the report to
430 #   - part->{no_branch_email}: a boolean indicating that we are missing a branch email
431 #   - part->{letter}: a GetPreparedLetter hash as returned by the C4::Letters module
432     my $report = $params->{report};
433     my $header = shift @{$report};
434     my $parts  = $report;
435
436     my @emails;
437     foreach my $part ( @{$parts} ) {
438
439         if ( $part->{status} || $params->{send_all} ) {
440
441             # We have a report to send, or we want to send even empty
442             # reports.
443
444             # Send to branch
445             my $addressee;
446             if ( $part->{email_address} ) {
447                 $addressee = $part->{email_address};
448             }
449             elsif ( !$part->{no_branch_email} ) {
450
451 #push @emails, "***We tried to send a branch report, but we have no email address for this branch.***\n\n";
452                 $addressee = C4::Context->preference('KohaAdminEmailAddress')
453                   if ( C4::Context->preference('KohaAdminEmailAddress') );
454             }
455
456             if ( $params->{send_email} ) {    # Only email if emails requested
457                 if ( defined($addressee) ) {
458                     C4::Letters::EnqueueLetter(
459                         {
460                             letter                 => $part->{letter},
461                             to_address             => $addressee,
462                             message_transport_type => 'email',
463                         }
464                       )
465                       or warn
466                       "can't enqueue letter $part->{letter} for $addressee";
467                 }
468
469                 # Copy to admin?
470                 if ( $params->{admin_email} ) {
471                     C4::Letters::EnqueueLetter(
472                         {
473                             letter                 => $part->{letter},
474                             to_address             => $params->{admin_email},
475                             message_transport_type => 'email',
476                         }
477                       )
478                       or warn
479 "can't enqueue letter $part->{letter} for $params->{admin_email}";
480                 }
481             }
482             else {
483                 $addressee ||=
484                   defined( $params->{admin_email} )
485                   ? $params->{admin_email} . "\n"
486                   : 'No recipient found' . "\n";
487                 my $email =
488                   "-------- Email message --------" . "\n\n";
489                 $email .= "To: $addressee\n";
490                 $email .= "Subject: "
491                   . $part->{letter}->{title} . "\n\n"
492                   . $part->{letter}->{content};
493                 push @emails, $email;
494             }
495         }
496     }
497
498     # Emit to stdout instead of email?
499     if ( !$params->{send_email} ) {
500
501         # The final message is the header + body of this part.
502         my $msg = $header;
503         $msg .= "No database updates have been performed.\n\n"
504           unless ( $params->{execute} );
505
506         # Append email reports to message
507         $msg .= join( "\n\n", @emails );
508         printf $msg;
509     }
510 }
511
512 #### Main Code
513
514 # Compile Stockrotation Report data
515 my $rotas = Koha::StockRotationRotas->search(undef,{ order_by => { '-asc' => 'title' }});
516 my $data  = $rotas->investigate;
517
518 # Perform db updates if requested
519 execute($data) if ($execute);
520
521 # Emit Reports
522 my $out_report = {};
523 $out_report = report_by_branch( $data, $branch ) if $report eq 'email';
524 $out_report = report_full( $data, $branch ) if $report eq 'full';
525 emit(
526     {
527         admin_email => $admin_email,
528         execute     => $execute,
529         report      => $out_report,
530         send_all    => $send_all,
531         send_email  => $send_email,
532     }
533 );
534
535 =head1 AUTHOR
536
537 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
538
539 =cut