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
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.
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.
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.
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
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
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.").
111 use Getopt::Long qw/HelpMessage :config gnu_getopt/;
114 use Koha::StockRotationRotas;
116 my $admin_email = '';
124 'admin-email|a=s' => \$admin_email,
125 'branchcode|b=s' => sub {
126 my ( $opt_name, $opt_value ) = @_;
127 if ( $opt_value eq 'all' ) {
131 my $branches = Koha::Libraries->search( {},
132 { order_by => { -asc => 'branchname' } } );
133 my $brnch = $branches->find($opt_value);
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 );
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;
155 printf("Option $opt_name should be either 'email' or 'full'.\n");
159 'send-all|S' => \$send_all,
160 'send-email|s' => \$send_email,
161 'help|h|?' => sub { HelpMessage }
165 $send_email++ if ($send_all); # if we send all, then we must want emails.
167 if ( $send_email && !$admin_email && ($report eq 'full')) {
168 printf("Sending the full report by email requires --admin-email.\n");
176 undef = execute($report);
178 Perform the database updates, within a transaction, that are reported as
179 needing to be performed by $REPORT.
181 $REPORT should be the return value of an invocation of `investigate`.
183 This procedure WILL mess with your database.
191 my $schema = Koha::Database->new->schema;
192 $schema->storage->txn_begin;
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;
200 elsif ( grep { $reason eq $_ } qw/in-demand advancement initiation/ ) {
201 $item->{object}->advance;
206 $schema->storage->txn_commit;
211 my $full_report = report_full($report);
213 Return an arrayref containing a string containing a detailed report about the
214 current state of the stockrotation subsystem.
216 $REPORT should be the return value of `investigate`.
218 No data in the database is manipulated by this procedure.
229 $header .= "STOCKROTATION REPORT\n";
230 $header .= "--------------------\n";
232 Total number of rotas: %5u
235 Total number of 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};
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} } ) {
257 join( "", map { _print_item($_) } @{ $rota->{items} } );
260 $body .= "\n No items to be processed for this rota.\n";
262 $body .= "\n Log:"; # Rota log details
263 if ( @{ $rota->{log} } ) {
264 $body .= join( "", map { _print_item($_) } @{ $rota->{log} } );
267 $body .= "\n No items in log for this rota.\n\n";
275 title => 'Stockrotation Report',
276 content => $body # The body of the report
278 status => 1, # We have a meaningful report
279 no_branch_email => 1, # We don't expect branch email in report
284 =head3 report_by_branch
286 my $email_report = report_by_branch($report, [$branch]);
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
295 =item $report should be the return value of `investigate`
297 =item $branch is optional and should be either 0 (to indicate 'all'), or a specific Koha::Library object.
301 No data in the database is manipulated by this procedure.
305 sub report_by_branch {
306 my ( $data, $branch ) = @_;
312 my $branched = $data->{branched};
315 $header .= "BRANCH-BASED STOCKROTATION REPORT\n";
316 $header .= "---------------------------------\n";
317 push @{$out}, $header;
319 if ($branch) { # Branch limited report
320 push @{$out}, _report_per_branch( $branched->{ $branch->branchcode } );
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} } );
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
337 =head3 _report_per_branch
339 my $branch_string = _report_per_branch($branch_details);
341 return a string containing details about the stockrotation items and their
342 status for the branch identified by $BRANCHCODE.
344 This helper procedure is only used from within `report_by_branch`.
346 No data in the database is manipulated by this procedure.
350 sub _report_per_branch {
354 if ( $branch && @{ $branch->{items} } ) {
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 }
370 email_address => $branch->{email},
379 my $string = _print_item($item_section);
381 Return a string containing an overview about $ITEM_SECTION.
383 This helper procedure is only used from within `report_full`.
385 No data in the database is manipulated by this procedure.
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;
409 undef = emit($params);
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
418 No data in the database is manipulated by this procedure.
420 The return value is unspecified: we simply emit a message as a side-effect or
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};
441 foreach my $part ( @{$parts} ) {
443 if ( $part->{status} || $params->{send_all} ) {
445 # We have a report to send, or we want to send even empty
448 # Select email address to send to
450 if ( $part->{email_address} ) {
451 $addressee = $part->{email_address};
453 elsif ( !$part->{no_branch_email} ) {
454 $addressee = C4::Context->preference('KohaAdminEmailAddress')
455 if ( C4::Context->preference('KohaAdminEmailAddress') );
458 if ( $params->{send_email} ) { # Only email if emails requested
459 if ( defined($addressee) ) {
460 C4::Letters::EnqueueLetter(
462 letter => $part->{letter},
463 to_address => $addressee,
464 message_transport_type => 'email',
468 "can't enqueue letter $part->{letter} for $addressee";
472 if ( $params->{admin_email} ) {
473 C4::Letters::EnqueueLetter(
475 letter => $part->{letter},
476 to_address => $params->{admin_email},
477 message_transport_type => 'email',
481 "can't enqueue letter $part->{letter} for $params->{admin_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;
498 # Emit to stdout instead of email?
499 if ( !$params->{send_email} ) {
501 # The final message is the header + body of this part.
503 $msg .= "No database updates have been performed.\n\n"
504 unless ( $params->{execute} );
506 # Append email reports to message
507 $msg .= join( "\n\n", @emails );
514 # Compile Stockrotation Report data
515 my $rotas = Koha::StockRotationRotas->search(undef,{ order_by => { '-asc' => 'title' }});
516 my $data = $rotas->investigate;
518 # Perform db updates if requested
519 execute($data) if ($execute);
523 $out_report = report_by_branch( $data, $branch ) if $report eq 'email';
524 $out_report = report_full( $data, $branch ) if $report eq 'full';
527 admin_email => $admin_email,
529 report => $out_report,
530 send_all => $send_all,
531 send_email => $send_email,
537 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>