]> git.koha-community.org Git - koha.git/blob - misc/cronjobs/stockrotation.pl
Bug 22566: (QA follow-up) Fix pod complaint
[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 use C4::Context;
113 use C4::Letters;
114 use Koha::StockRotationRotas;
115
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;
122
123 my $ok = GetOptions(
124     'admin-email|a=s' => \$admin_email,
125     'branchcode|b=s'  => sub {
126         my ( $opt_name, $opt_value ) = @_;
127         if ( $opt_value eq 'all' ) {
128             $branch = 0;
129         }
130         else {
131             my $branches = Koha::Libraries->search( {},
132                 { order_by => { -asc => 'branchname' } } );
133             my $brnch = $branches->find($opt_value);
134             if ($brnch) {
135                 $branch = $brnch;
136                 return $brnch;
137             }
138             else {
139                 printf("Option $opt_name should be one of (name -> code):\n");
140                 while ( my $candidate = $branches->next ) {
141                     printf( "  %-40s  ->  %s\n",
142                         $candidate->branchname, $candidate->branchcode );
143                 }
144                 exit 1;
145             }
146         }
147     },
148     'execute|x'  => \$execute,
149     'report|r=s' => sub {
150         my ( $opt_name, $opt_value ) = @_;
151         if ( $opt_value eq 'full' || $opt_value eq 'email' ) {
152             $report = $opt_value;
153         }
154         else {
155             printf("Option $opt_name should be either 'email' or 'full'.\n");
156             exit 1;
157         }
158     },
159     'send-all|S'   => \$send_all,
160     'send-email|s' => \$send_email,
161     'help|h|?'     => sub { HelpMessage }
162 );
163 exit 1 unless ($ok);
164
165 $send_email++ if ($send_all);    # if we send all, then we must want emails.
166
167 if ( $send_email && !$admin_email && ($report eq 'full')) {
168     printf("Sending the full report by email requires --admin-email.\n");
169     exit 1;
170 }
171
172 =head2 Helpers
173
174 =head3 execute
175
176   undef = execute($report);
177
178 Perform the database updates, within a transaction, that are reported as
179 needing to be performed by $REPORT.
180
181 $REPORT should be the return value of an invocation of `investigate`.
182
183 This procedure WILL mess with your database.
184
185 =cut
186
187 sub execute {
188     my ($data) = @_;
189
190     # Begin transaction
191     my $schema = Koha::Database->new->schema;
192     $schema->storage->txn_begin;
193
194     # Carry out db updates
195     foreach my $item ( @{ $data->{items} } ) {
196         my $reason = $item->{reason};
197         if ( $reason eq 'repatriation' ) {
198             $item->{object}->repatriate;
199         }
200         elsif ( grep { $reason eq $_ } qw/in-demand advancement initiation/ ) {
201             $item->{object}->advance;
202         }
203     }
204
205     # End transaction
206     $schema->storage->txn_commit;
207 }
208
209 =head3 report_full
210
211   my $full_report = report_full($report);
212
213 Return an arrayref containing a string containing a detailed report about the
214 current state of the stockrotation subsystem.
215
216 $REPORT should be the return value of `investigate`.
217
218 No data in the database is manipulated by this procedure.
219
220 =cut
221
222 sub report_full {
223     my ($data) = @_;
224
225     my $header = "";
226     my $body   = "";
227
228     # Summary
229     $header .= "STOCKROTATION REPORT\n";
230     $header .= "--------------------\n";
231     $body   .= sprintf "
232   Total number of rotas:         %5u
233     Inactive rotas:              %5u
234     Active rotas:                %5u
235   Total number of items:         %5u
236     Inactive items:              %5u
237     Stationary items:            %5u
238     Actionable items:            %5u
239   Total items to be initiated:   %5u
240   Total items to be repatriated: %5u
241   Total items to be advanced:    %5u
242   Total items in demand:         %5u\n\n",
243       $data->{sum_rotas},  $data->{rotas_inactive}, $data->{rotas_active},
244       $data->{sum_items},  $data->{items_inactive}, $data->{stationary},
245       $data->{actionable}, $data->{initiable},      $data->{repatriable},
246       $data->{advanceable}, $data->{indemand};
247
248     if ( @{ $data->{rotas} } ) {    # Per Rota details
249         $body .= "ROTAS DETAIL\n";
250         $body .= "------------\n\n";
251         foreach my $rota ( @{ $data->{rotas} } ) {
252             $body .= sprintf "Details for %s [%s]:\n",
253               $rota->{name}, $rota->{id};
254             $body .= "\n  Items:";    # Rota item details
255             if ( @{ $rota->{items} } ) {
256                 $body .=
257                   join( "", map { _print_item($_) } @{ $rota->{items} } );
258             }
259             else {
260                 $body .= "\n    No items to be processed for this rota.\n";
261             }
262             $body .= "\n  Log:";      # Rota log details
263             if ( @{ $rota->{log} } ) {
264                 $body .= join( "", map { _print_item($_) } @{ $rota->{log} } );
265             }
266             else {
267                 $body .= "\n    No items in log for this rota.\n\n";
268             }
269         }
270     }
271     return [
272         $header,
273         {
274             letter => {
275                 title   => 'Stockrotation Report',
276                 content => $body                     # The body of the report
277             },
278             status          => 1,    # We have a meaningful report
279             no_branch_email => 1,    # We don't expect branch email in report
280         }
281     ];
282 }
283
284 =head3 report_by_branch
285
286   my $email_report = report_by_branch($report, [$branch]);
287
288 Returns an arrayref containing a header string, with basic report information,
289 and any number of 'per_branch' strings, containing a detailed report about the
290 current state of the stockrotation subsystem, from the perspective of those
291 individual branches.
292
293 =over 2
294
295 =item $report should be the return value of `investigate`
296
297 =item $branch is optional and should be either 0 (to indicate 'all'), or a specific Koha::Library object.
298
299 =back
300
301 No data in the database is manipulated by this procedure.
302
303 =cut
304
305 sub report_by_branch {
306     my ( $data, $branch ) = @_;
307
308     my $out    = [];
309     my $header = "";
310
311     # Summary
312     my $branched = $data->{branched};
313     my $flag     = 0;
314
315     $header .= "BRANCH-BASED STOCKROTATION REPORT\n";
316     $header .= "---------------------------------\n";
317     push @{$out}, $header;
318
319     if ($branch) {    # Branch limited report
320         push @{$out}, _report_per_branch( $branched->{ $branch->branchcode } );
321     }
322     elsif ( $data->{actionable} ) {    # Full email report
323         while ( my ( $branchcode_id, $details ) = each %{$branched} ) {
324             push @{$out}, _report_per_branch($details)
325               if ( @{ $details->{items} } );
326         }
327     }
328     else {
329         push @{$out}, {
330             body => "No actionable items at any libraries.\n\n",    # The body of the report
331             no_branch_email => 1,    # We don't expect branch email in report
332         };
333     }
334     return $out;
335 }
336
337 =head3 _report_per_branch
338
339   my $branch_string = _report_per_branch($branch_details);
340
341 return a string containing details about the stockrotation items and their
342 status for the branch identified by $BRANCHCODE.
343
344 This helper procedure is only used from within `report_by_branch`.
345
346 No data in the database is manipulated by this procedure.
347
348 =cut
349
350 sub _report_per_branch {
351     my ($branch) = @_;
352
353     my $status = 0;
354     if ( $branch && @{ $branch->{items} } ) {
355         $status = 1;
356     }
357
358     if (
359         my $letter = C4::Letters::GetPreparedLetter(
360             module                 => 'circulation',
361             letter_code            => "SR_SLIP",
362             branchcode             => $branch->{code},
363             message_transport_type => 'email',
364             substitute             => { branch => $branch }
365         )
366       )
367     {
368         return {
369             letter        => $letter,
370             email_address => $branch->{email},
371             status        => $status
372         };
373     }
374     return;
375 }
376
377 =head3 _print_item
378
379   my $string = _print_item($item_section);
380
381 Return a string containing an overview about $ITEM_SECTION.
382
383 This helper procedure is only used from within `report_full`.
384
385 No data in the database is manipulated by this procedure.
386
387 =cut
388
389 sub _print_item {
390     my ($item) = @_;
391     return sprintf "
392     Title:           %s
393     Author:          %s
394     Callnumber:      %s
395     Location:        %s
396     Barcode:         %s
397     On loan?:        %s
398     Status:          %s
399     Current Library: %s [%s]\n\n",
400       $item->{title}      || "N/A", $item->{author}   || "N/A",
401       $item->{callnumber} || "N/A", $item->{location} || "N/A",
402       $item->{barcode} || "N/A", $item->{onloan} ? 'Yes' : 'No',
403       $item->{reason} || "N/A", $item->{branch}->branchname,
404       $item->{branch}->branchcode;
405 }
406
407 =head3 emit
408
409   undef = emit($params);
410
411 $PARAMS should be a hashref of the following format:
412   admin_email: the address to which a copy of all reports should be sent.
413   execute: the flag indicating whether we performed db updates
414   send_all: the flag indicating whether we should send even empty reports
415   send_email: the flag indicating whether we want to emit to stdout or email
416   report: the data structure returned from one of the report procedures
417
418 No data in the database is manipulated by this procedure.
419
420 The return value is unspecified: we simply emit a message as a side-effect or
421 die.
422
423 =cut
424
425 sub emit {
426     my ($params) = @_;
427
428 # REPORT is an arrayref of at least 2 elements:
429 #   - The header for the report, which will be repeated for each part
430 #   - a "part" for each report we want to emit
431 # PARTS are hashrefs:
432 #   - part->{status}: a boolean indicating whether the reported part is empty or not
433 #   - part->{email_address}: the email address to send the report to
434 #   - part->{no_branch_email}: a boolean indicating that we are missing a branch email
435 #   - part->{letter}: a GetPreparedLetter hash as returned by the C4::Letters module
436     my $report = $params->{report};
437     my $header = shift @{$report};
438     my $parts  = $report;
439
440     my @emails;
441     foreach my $part ( @{$parts} ) {
442
443         if ( $part->{status} || $params->{send_all} ) {
444
445             # We have a report to send, or we want to send even empty
446             # reports.
447
448             # Select email address to send to
449             my $addressee;
450             if ( $part->{email_address} ) {
451                 $addressee = $part->{email_address};
452             }
453             elsif ( !$part->{no_branch_email} ) {
454                 $addressee = C4::Context->preference('KohaAdminEmailAddress')
455                   if ( C4::Context->preference('KohaAdminEmailAddress') );
456             }
457
458             if ( $params->{send_email} ) {    # Only email if emails requested
459                 if ( defined($addressee) ) {
460                     C4::Letters::EnqueueLetter(
461                         {
462                             letter                 => $part->{letter},
463                             to_address             => $addressee,
464                             message_transport_type => 'email',
465                         }
466                       )
467                       or warn
468                       "can't enqueue letter $part->{letter} for $addressee";
469                 }
470
471                 # Copy to admin?
472                 if ( $params->{admin_email} ) {
473                     C4::Letters::EnqueueLetter(
474                         {
475                             letter                 => $part->{letter},
476                             to_address             => $params->{admin_email},
477                             message_transport_type => 'email',
478                         }
479                       )
480                       or warn
481                       "can't enqueue letter $part->{letter} for $params->{admin_email}";
482                 }
483             }
484             else {
485                 my $email =
486                   "-------- Email message --------" . "\n\n";
487                 $email .= "To: $addressee\n";
488                 $email .= "Cc: " . $params->{admin_email} . "\n"
489                   if ( $params->{admin_email} );
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