Bug 17600: Standardize our EXPORT_OK
[srvgit] / misc / cronjobs / advance_notices.pl
1 #!/usr/bin/perl
2
3 # Copyright 2008 LibLime
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 =head1 NAME
21
22 advance_notices.pl - prepare messages to be sent to patrons for nearly due, or due, items
23
24 =head1 SYNOPSIS
25
26        advance_notices.pl
27          [ -n ][ -m <number of days> ][ --itemscontent <comma separated field list> ][ -c ]
28
29 =head1 DESCRIPTION
30
31 This script prepares pre-due and item due reminders to be sent to
32 patrons. It queues them in the message queue, which is processed by
33 the process_message_queue.pl cronjob. The type and timing of the
34 messages can be configured by the patrons in their "My Alerts" tab in
35 the OPAC.
36
37 =cut
38
39 use strict;
40 use warnings;
41 use Getopt::Long qw( GetOptions );
42 use Pod::Usage qw( pod2usage );
43 BEGIN {
44     # find Koha's Perl modules
45     # test carefully before changing this
46     use FindBin ();
47     eval { require "$FindBin::Bin/../kohalib.pl" };
48 }
49 use Koha::Script -cron;
50 use C4::Context;
51 use C4::Letters;
52 use C4::Members;
53 use C4::Members::Messaging;
54 use C4::Log qw( cronlogaction );
55 use Koha::Items;
56 use Koha::Libraries;
57 use Koha::Patrons;
58
59 =head1 OPTIONS
60
61 =over 8
62
63 =item B<--help>
64
65 Print a brief help message and exits.
66
67 =item B<--man>
68
69 Prints the manual page and exits.
70
71 =item B<-v>
72
73 Verbose. Without this flag set, only fatal errors are reported.
74
75 =item B<-n>
76
77 Do not send any email. Advanced or due notices that would have been sent to
78 the patrons are printed to standard out.
79
80 =item B<-m>
81
82 Defines the maximum number of days in advance to send advance notices.
83
84 =item B<-c>
85
86 Confirm flag: Add this option. The script will only print a usage
87 statement otherwise.
88
89 =item B<--itemscontent>
90
91 comma separated list of fields that get substituted into templates in
92 places of the E<lt>E<lt>items.contentE<gt>E<gt> placeholder. This
93 defaults to date_due,title,author,barcode
94
95 Other possible values come from fields in the biblios, items and
96 issues tables.
97
98 =item B<--digest-per-branch>
99
100 Flag to indicate that generation of message digests should be
101 performed separately for each branch.
102
103 A patron could potentially have loans at several different branches
104 There is no natural branch to set as the sender on the aggregated
105 message in this situation so the default behavior is to use the
106 borrowers home branch.  This could surprise to the borrower when
107 message sender is a library where they have not borrowed anything.
108
109 Enabling this flag ensures that the issuing library is the sender of
110 the digested message.  It has no effect unless the borrower has
111 chosen 'Digests only' on the advance messages.
112
113 =item B<--library>
114
115 select notices for one specific library. Use the value in the
116 branches.branchcode table. This option can be repeated in order
117 to select notices for a group of libraries.
118
119 =item B<--frombranch>
120
121 Set the from address for the notice to one of 'item-homebranch' or 'item-issuebranch'.
122
123 Defaults to 'item-issuebranch'
124
125 =back
126
127 =head2 Configuration
128
129 This script pays attention to the advanced notice configuration
130 performed by borrowers in the OPAC, or by staff in the patron detail page of the intranet. The content of the messages is configured in Tools -> Notices and slips. Advanced notices use the PREDUE template, due notices use DUE. More information about the use of this
131 section of Koha is available in the Koha manual.
132
133 =head2 Outgoing emails
134
135 Typically, messages are prepared for each patron with due
136 items, and who have selected (or the library has elected for them) Advance or Due notices.
137
138 These emails are staged in the outgoing message queue, as are messages
139 produced by other features of Koha. This message queue must be
140 processed regularly by the
141 F<misc/cronjobs/process_message_queue.pl> program.
142
143 In the event that the C<-n> flag is passed to this program, no emails
144 are sent. Instead, messages are sent on standard output from this
145 program. They may be redirected to a file if desired.
146
147 =head2 Templates
148
149 Templates can contain variables enclosed in double angle brackets like
150 E<lt>E<lt>thisE<gt>E<gt>. Those variables will be replaced with values
151 specific to the overdue items or relevant patron. Available variables
152 are:
153
154 =over
155
156 =item E<lt>E<lt>items.contentE<gt>E<gt>
157
158 one line for each item, each line containing a tab separated list of
159 date due, title, author, barcode
160
161 =item E<lt>E<lt>borrowers.*E<gt>E<gt>
162
163 any field from the borrowers table
164
165 =item E<lt>E<lt>branches.*E<gt>E<gt>
166
167 any field from the branches table
168
169 =back
170
171 =head1 SEE ALSO
172
173 The F<misc/cronjobs/overdue_notices.pl> program allows you to send
174 messages to patrons when their messages are overdue.
175
176 =cut
177
178 binmode( STDOUT, ':encoding(UTF-8)' );
179
180 # These are defaults for command line options.
181 my $confirm;                                                        # -c: Confirm that the user has read and configured this script.
182 my $nomail;                                                         # -n: No mail. Will not send any emails.
183 my $mindays     = 0;                                                # -m: Maximum number of days in advance to send notices
184 my $maxdays     = 30;                                               # -e: the End of the time period
185 my $verbose     = 0;                                                # -v: verbose
186 my $digest_per_branch = 0;                                          # -digest-per-branch: Prepare and send digests per branch
187 my @branchcodes; # Branch(es) passed as parameter
188 my $frombranch   = 'item-issuebranch';
189 my $itemscontent = join(',',qw( date_due title author barcode ));
190
191 my $help    = 0;
192 my $man     = 0;
193
194 GetOptions(
195             'help|?'         => \$help,
196             'man'            => \$man,
197             'library=s'      => \@branchcodes,
198             'frombranch=s'   => \$frombranch,
199             'c'              => \$confirm,
200             'n'              => \$nomail,
201             'm:i'            => \$maxdays,
202             'v'              => \$verbose,
203             'digest-per-branch' => \$digest_per_branch,
204             'itemscontent=s' => \$itemscontent,
205        )or pod2usage(2);
206 pod2usage(1) if $help;
207 pod2usage( -verbose => 2 ) if $man;
208
209 # Since advance notice options are not visible in the web-interface
210 # unless EnhancedMessagingPreferences is on, let the user know that
211 # this script probably isn't going to do much
212 if ( ! C4::Context->preference('EnhancedMessagingPreferences') ) {
213     warn <<'END_WARN';
214
215 The "EnhancedMessagingPreferences" syspref is off.
216 Therefore, it is unlikely that this script will actually produce any messages to be sent.
217 To change this, edit the "EnhancedMessagingPreferences" syspref.
218
219 END_WARN
220 }
221 unless ($confirm) {
222      pod2usage(1);
223 }
224 cronlogaction();
225
226 my %branches = ();
227 if (@branchcodes) {
228     %branches = map { $_ => 1 } @branchcodes;
229 }
230
231 die "--frombranch takes item-homebranch or item-issuebranch only"
232   unless ( $frombranch eq 'item-issuebranch'
233     || $frombranch eq 'item-homebranch' );
234 my $owning_library = ( $frombranch eq 'item-homebranch' ) ? 1 : 0;
235
236 # The fields that will be substituted into <<items.content>>
237 my @item_content_fields = split(/,/,$itemscontent);
238
239 warn 'getting upcoming due issues' if $verbose;
240 my $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( {
241     days_in_advance => $maxdays,
242     owning_library => $owning_library
243  } );
244 warn 'found ' . scalar( @$upcoming_dues ) . ' issues' if $verbose;
245
246 # hash of borrowernumber to number of items upcoming
247 # for patrons wishing digests only.
248 my $upcoming_digest = {};
249 my $due_digest = {};
250
251 my $dbh = C4::Context->dbh();
252 my $sth = $dbh->prepare(<<'END_SQL');
253 SELECT biblio.*, items.*, issues.*
254   FROM issues,items,biblio
255   WHERE items.itemnumber=issues.itemnumber
256     AND biblio.biblionumber=items.biblionumber
257     AND issues.borrowernumber = ?
258     AND issues.itemnumber = ?
259     AND (TO_DAYS(date_due)-TO_DAYS(NOW()) = ?)
260 END_SQL
261
262 my $admin_adress = C4::Context->preference('KohaAdminEmailAddress');
263
264 my @letters;
265 UPCOMINGITEM: foreach my $upcoming ( @$upcoming_dues ) {
266     @letters = ();
267     warn 'examining ' . $upcoming->{'itemnumber'} . ' upcoming due items' if $verbose;
268
269     my $from_address = $upcoming->{branchemail} || $admin_adress;
270
271     my $borrower_preferences;
272     if ( 0 == $upcoming->{'days_until_due'} ) {
273         # This item is due today. Send an 'item due' message.
274         $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $upcoming->{'borrowernumber'},
275                                                                                    message_name   => 'item_due' } );
276         next unless $borrower_preferences;
277         
278         if ( $borrower_preferences->{'wants_digest'} ) {
279             # cache this one to process after we've run through all of the items.
280             if ($digest_per_branch) {
281                 $due_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{email} = $from_address;
282                 $due_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{count}++;
283             } else {
284                 $due_digest->{ $upcoming->{borrowernumber} }->{email} = $from_address;
285                 $due_digest->{ $upcoming->{borrowernumber} }->{count}++;
286             }
287         } else {
288             my $branchcode;
289             if($owning_library) {
290                 $branchcode = $upcoming->{'homebranch'};
291             } else {
292                 $branchcode = $upcoming->{'branchcode'};
293             }
294             # Skip this DUE if we specify list of libraries and this one is not part of it
295             next if (@branchcodes && !$branches{$branchcode});
296
297             my $item = Koha::Items->find( $upcoming->{itemnumber} );
298             my $letter_type = 'DUE';
299             $sth->execute($upcoming->{'borrowernumber'},$upcoming->{'itemnumber'},'0');
300             my $titles = "";
301             while ( my $item_info = $sth->fetchrow_hashref()) {
302                 $titles .= C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields } );
303             }
304
305             ## Get branch info for borrowers home library.
306             foreach my $transport ( keys %{$borrower_preferences->{'transports'}} ) {
307                 my $letter = parse_letter( { letter_code    => $letter_type,
308                                       borrowernumber => $upcoming->{'borrowernumber'},
309                                       branchcode     => $branchcode,
310                                       biblionumber   => $item->biblionumber,
311                                       itemnumber     => $upcoming->{'itemnumber'},
312                                       substitute     => { 'items.content' => $titles },
313                                       message_transport_type => $transport,
314                                     } )
315                     or warn "no letter of type '$letter_type' found for borrowernumber ".$upcoming->{'borrowernumber'}.". Please see sample_notices.sql";
316                 push @letters, $letter if $letter;
317             }
318         }
319     } else {
320         $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $upcoming->{'borrowernumber'},
321                                                                                    message_name   => 'advance_notice' } );
322         next UPCOMINGITEM unless $borrower_preferences && exists $borrower_preferences->{'days_in_advance'};
323         next UPCOMINGITEM unless $borrower_preferences->{'days_in_advance'} == $upcoming->{'days_until_due'};
324
325         if ( $borrower_preferences->{'wants_digest'} ) {
326             # cache this one to process after we've run through all of the items.
327             if ($digest_per_branch) {
328                 $upcoming_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{email} = $from_address;
329                 $upcoming_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{count}++;
330             } else {
331                 $upcoming_digest->{ $upcoming->{borrowernumber} }->{email} = $from_address;
332                 $upcoming_digest->{ $upcoming->{borrowernumber} }->{count}++;
333             }
334         } else {
335             my $branchcode;
336             if($owning_library) {
337             $branchcode = $upcoming->{'homebranch'};
338             } else {
339             $branchcode = $upcoming->{'branchcode'};
340             }
341             # Skip this PREDUE if we specify list of libraries and this one is not part of it
342             next if (@branchcodes && !$branches{$branchcode});
343
344             my $item = Koha::Items->find( $upcoming->{itemnumber} );
345             my $letter_type = 'PREDUE';
346             $sth->execute($upcoming->{'borrowernumber'},$upcoming->{'itemnumber'},$borrower_preferences->{'days_in_advance'});
347             my $titles = "";
348             while ( my $item_info = $sth->fetchrow_hashref()) {
349                 $titles .= C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields } );
350             }
351
352             ## Get branch info for borrowers home library.
353             foreach my $transport ( keys %{$borrower_preferences->{'transports'}} ) {
354                 my $letter = parse_letter( { letter_code    => $letter_type,
355                                       borrowernumber => $upcoming->{'borrowernumber'},
356                                       branchcode     => $branchcode,
357                                       biblionumber   => $item->biblionumber,
358                                       itemnumber     => $upcoming->{'itemnumber'},
359                                       substitute     => { 'items.content' => $titles },
360                                       message_transport_type => $transport,
361                                     } )
362                     or warn "no letter of type '$letter_type' found for borrowernumber ".$upcoming->{'borrowernumber'}.". Please see sample_notices.sql";
363                 push @letters, $letter if $letter;
364             }
365         }
366     }
367
368     # If we have prepared a letter, send it.
369     if ( @letters ) {
370       if ($nomail) {
371         for my $letter ( @letters ) {
372             local $, = "\f";
373             print $letter->{'content'}."\n";
374         }
375       }
376       else {
377         for my $letter ( @letters ) {
378             C4::Letters::EnqueueLetter( { letter                 => $letter,
379                                           borrowernumber         => $upcoming->{'borrowernumber'},
380                                           from_address           => $from_address,
381                                           message_transport_type => $letter->{message_transport_type} } );
382         }
383       }
384     }
385 }
386
387
388
389 # Now, run through all the people that want digests and send them
390
391 my $sth_digest = $dbh->prepare(<<'END_SQL');
392 SELECT biblio.*, items.*, issues.*
393   FROM issues,items,biblio
394   WHERE items.itemnumber=issues.itemnumber
395     AND biblio.biblionumber=items.biblionumber
396     AND issues.borrowernumber = ?
397     AND (TO_DAYS(date_due)-TO_DAYS(NOW()) = ?)
398 END_SQL
399
400 if ($digest_per_branch) {
401     while (my ($branchcode, $digests) = each %$upcoming_digest) {
402         send_digests({
403             sth => $sth_digest,
404             digests => $digests,
405             letter_code => 'PREDUEDGST',
406             message_name => 'advance_notice',
407             branchcode => $branchcode,
408             get_item_info => sub {
409                 my $params = shift;
410                 $params->{sth}->execute($params->{borrowernumber},
411                                         $params->{borrower_preferences}->{'days_in_advance'});
412                 return sub {
413                     $params->{sth}->fetchrow_hashref;
414                 };
415             }
416         });
417     }
418
419     while (my ($branchcode, $digests) = each %$due_digest) {
420         send_digests({
421             sth => $sth_digest,
422             digests => $due_digest,
423             letter_code => 'DUEDGST',
424             branchcode => $branchcode,
425             message_name => 'item_due',
426             get_item_info => sub {
427                 my $params = shift;
428                 $params->{sth}->execute($params->{borrowernumber}, 0);
429                 return sub {
430                     $params->{sth}->fetchrow_hashref;
431                 };
432             }
433         });
434     }
435 } else {
436     send_digests({
437         sth => $sth_digest,
438         digests => $upcoming_digest,
439         letter_code => 'PREDUEDGST',
440         message_name => 'advance_notice',
441         get_item_info => sub {
442             my $params = shift;
443             $params->{sth}->execute($params->{borrowernumber},
444                                     $params->{borrower_preferences}->{'days_in_advance'});
445             return sub {
446                 $params->{sth}->fetchrow_hashref;
447             };
448         }
449     });
450
451     send_digests({
452         sth => $sth_digest,
453         digests => $due_digest,
454         letter_code => 'DUEDGST',
455         message_name => 'item_due',
456         get_item_info => sub {
457             my $params = shift;
458             $params->{sth}->execute($params->{borrowernumber}, 0);
459             return sub {
460                 $params->{sth}->fetchrow_hashref;
461             };
462         }
463     });
464 }
465
466 =head1 METHODS
467
468 =head2 parse_letter
469
470 =cut
471
472 sub parse_letter {
473     my $params = shift;
474
475     foreach my $required ( qw( letter_code borrowernumber ) ) {
476         return unless exists $params->{$required};
477     }
478     my $patron = Koha::Patrons->find( $params->{borrowernumber} );
479
480     my %table_params = ( 'borrowers' => $params->{'borrowernumber'} );
481
482     if ( my $p = $params->{'branchcode'} ) {
483         $table_params{'branches'} = $p;
484     }
485     if ( my $p = $params->{'itemnumber'} ) {
486         $table_params{'issues'} = $p;
487         $table_params{'items'} = $p;
488     }
489     if ( my $p = $params->{'biblionumber'} ) {
490         $table_params{'biblio'} = $p;
491         $table_params{'biblioitems'} = $p;
492     }
493
494     return C4::Letters::GetPreparedLetter (
495         module => 'circulation',
496         letter_code => $params->{'letter_code'},
497         branchcode => $table_params{'branches'},
498         lang => $patron->lang,
499         substitute => $params->{'substitute'},
500         tables     => \%table_params,
501         message_transport_type => $params->{message_transport_type},
502     );
503 }
504
505 =head2 get_branch_info
506
507 =cut
508
509 sub get_branch_info {
510     my ( $borrowernumber ) = @_;
511
512     ## Get branch info for borrowers home library.
513     my $patron = Koha::Patrons->find( $borrowernumber );
514     my $branch = $patron->library->unblessed;
515     my %branch_info;
516     foreach my $key( keys %$branch ) {
517         $branch_info{"branches.$key"} = $branch->{$key};
518     }
519
520     return %branch_info;
521 }
522
523 =head2 send_digests
524
525     send_digests({
526         digests => ...,
527         sth => ...,
528         letter_code => ...,
529         get_item_info => ...,
530     })
531
532 Enqueue digested letters (or print them if -n was passed at command line).
533
534 Parameters:
535
536 =over 4
537
538 =item C<$digests>
539
540 Reference to the array of digested messages.
541
542 =item C<$sth>
543
544 Prepared statement handle for fetching overdue issues.
545
546 =item C<$letter_code>
547
548 String that denote the letter code.
549
550 =item C<$get_item_info>
551
552 Subroutine for executing prepared statement.  Takes parameters $sth,
553 $borrowernumber and $borrower_parameters and return a generator
554 function that produce the matching rows.
555
556 =back
557
558 =cut
559
560 sub send_digests {
561     my $params = shift;
562
563     PATRON: while ( my ( $borrowernumber, $digest ) = each %{$params->{digests}} ) {
564         @letters = ();
565         my $count = $digest->{count};
566         my $from_address = $digest->{email};
567
568         my %branch_info;
569         my $branchcode;
570
571         if (defined($params->{branchcode})) {
572             %branch_info = ();
573             $branchcode = $params->{branchcode};
574         } else {
575             ## Get branch info for borrowers home library.
576             %branch_info = get_branch_info( $borrowernumber );
577             $branchcode = $branch_info{'branches.branchcode'};
578         }
579
580         my $borrower_preferences =
581             C4::Members::Messaging::GetMessagingPreferences(
582                 {
583                     borrowernumber => $borrowernumber,
584                     message_name   => $params->{message_name}
585                 }
586             );
587
588         next PATRON unless $borrower_preferences; # how could this happen?
589
590         my $next_item_info = $params->{get_item_info}->({
591             sth => $params->{sth},
592             borrowernumber => $borrowernumber,
593             borrower_preferences => $borrower_preferences
594         });
595         my $titles = "";
596         while ( my $item_info = $next_item_info->()) {
597             $titles .= C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields } );
598         }
599
600         foreach my $transport ( keys %{ $borrower_preferences->{'transports'} } ) {
601             my $letter = parse_letter(
602                 {
603                     letter_code    => $params->{letter_code},
604                     borrowernumber => $borrowernumber,
605                     substitute     => {
606                         count           => $count,
607                         'items.content' => $titles,
608                         %branch_info
609                     },
610                     branchcode     => $branchcode,
611                     message_transport_type => $transport
612                 }
613             );
614             unless ( $letter ){
615                 warn "no letter of type '$params->{letter_type}' found for borrowernumber $borrowernumber. Please see sample_notices.sql";
616                 next;
617             }
618             push @letters, $letter if $letter;
619         }
620
621         if ( @letters ) {
622             if ($nomail) {
623                 for my $letter ( @letters ) {
624                     local $, = "\f";
625                     print $letter->{'content'};
626                 }
627             }
628             else {
629                 for my $letter ( @letters ) {
630                     C4::Letters::EnqueueLetter( { letter                 => $letter,
631                                                   borrowernumber         => $borrowernumber,
632                                                   from_address           => $from_address,
633                                                   message_transport_type => $letter->{message_transport_type} } );
634                 }
635             }
636         }
637     }
638 }
639
640
641 1;
642
643 __END__