Bug 15504: Remove PatronLastActivity preference
[koha.git] / 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 use Koha::Script -cron;
44 use C4::Context;
45 use C4::Letters;
46 use C4::Members;
47 use C4::Members::Messaging;
48 use C4::Log qw( cronlogaction );
49 use Koha::Items;
50 use Koha::Libraries;
51 use Koha::Patrons;
52
53 =head1 OPTIONS
54
55 =over 8
56
57 =item B<--help>
58
59 Print a brief help message and exits.
60
61 =item B<--man>
62
63 Prints the manual page and exits.
64
65 =item B<-v>
66
67 Verbose. Without this flag set, only fatal errors are reported.
68
69 =item B<-n>
70
71 Do not send any email. Advanced or due notices that would have been sent to
72 the patrons are printed to standard out.
73
74 =item B<-m>
75
76 Defines the maximum number of days in advance to send advance notices.
77
78 =item B<-c>
79
80 Confirm flag: Add this option. The script will only print a usage
81 statement otherwise.
82
83 =item B<--itemscontent>
84
85 comma separated list of fields that get substituted into templates in
86 places of the E<lt>E<lt>items.contentE<gt>E<gt> placeholder. This
87 defaults to date_due,title,author,barcode
88
89 Other possible values come from fields in the biblios, items and
90 issues tables.
91
92 =item B<--digest-per-branch>
93
94 Flag to indicate that generation of message digests should be
95 performed separately for each branch.
96
97 A patron could potentially have loans at several different branches
98 There is no natural branch to set as the sender on the aggregated
99 message in this situation so the default behavior is to use the
100 borrowers home branch.  This could surprise to the borrower when
101 message sender is a library where they have not borrowed anything.
102
103 Enabling this flag ensures that the issuing library is the sender of
104 the digested message.  It has no effect unless the borrower has
105 chosen 'Digests only' on the advance messages.
106
107 =item B<--library>
108
109 select notices for one specific library. Use the value in the
110 branches.branchcode table. This option can be repeated in order
111 to select notices for a group of libraries.
112
113 =item B<--frombranch>
114
115 Set the from address for the notice to one of 'item-homebranch' or 'item-issuebranch'.
116
117 Defaults to 'item-issuebranch'
118
119 =back
120
121 =head2 Configuration
122
123 This script pays attention to the advanced notice configuration
124 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
125 section of Koha is available in the Koha manual.
126
127 =head2 Outgoing emails
128
129 Typically, messages are prepared for each patron with due
130 items, and who have selected (or the library has elected for them) Advance or Due notices.
131
132 These emails are staged in the outgoing message queue, as are messages
133 produced by other features of Koha. This message queue must be
134 processed regularly by the
135 F<misc/cronjobs/process_message_queue.pl> program.
136
137 In the event that the C<-n> flag is passed to this program, no emails
138 are sent. Instead, messages are sent on standard output from this
139 program. They may be redirected to a file if desired.
140
141 =head2 Templates
142
143 Templates can contain variables enclosed in double angle brackets like
144 E<lt>E<lt>thisE<gt>E<gt>. Those variables will be replaced with values
145 specific to the overdue items or relevant patron. Available variables
146 are:
147
148 =over
149
150 =item E<lt>E<lt>items.contentE<gt>E<gt>
151
152 one line for each item, each line containing a tab separated list of
153 date due, title, author, barcode
154
155 =item E<lt>E<lt>borrowers.*E<gt>E<gt>
156
157 any field from the borrowers table
158
159 =item E<lt>E<lt>branches.*E<gt>E<gt>
160
161 any field from the branches table
162
163 =back
164
165 =head1 SEE ALSO
166
167 The F<misc/cronjobs/overdue_notices.pl> program allows you to send
168 messages to patrons when their messages are overdue.
169
170 =cut
171
172 binmode( STDOUT, ':encoding(UTF-8)' );
173
174 # These are defaults for command line options.
175 my $confirm;                                                        # -c: Confirm that the user has read and configured this script.
176 my $nomail;                                                         # -n: No mail. Will not send any emails.
177 my $mindays     = 0;                                                # -m: Maximum number of days in advance to send notices
178 my $maxdays     = 30;                                               # -e: the End of the time period
179 my $verbose     = 0;                                                # -v: verbose
180 my $digest_per_branch = 0;                                          # -digest-per-branch: Prepare and send digests per branch
181 my @branchcodes; # Branch(es) passed as parameter
182 my $frombranch   = 'item-issuebranch';
183 my $itemscontent = join(',',qw( date_due title author barcode ));
184
185 my $help    = 0;
186 my $man     = 0;
187
188 my $command_line_options = join(" ",@ARGV);
189
190 GetOptions(
191             'help|?'         => \$help,
192             'man'            => \$man,
193             'library=s'      => \@branchcodes,
194             'frombranch=s'   => \$frombranch,
195             'c'              => \$confirm,
196             'n'              => \$nomail,
197             'm:i'            => \$maxdays,
198             'v'              => \$verbose,
199             'digest-per-branch' => \$digest_per_branch,
200             'itemscontent=s' => \$itemscontent,
201        )or pod2usage(2);
202 pod2usage(1) if $help;
203 pod2usage( -verbose => 2 ) if $man;
204
205 # Since advance notice options are not visible in the web-interface
206 # unless EnhancedMessagingPreferences is on, let the user know that
207 # this script probably isn't going to do much
208 if ( ! C4::Context->preference('EnhancedMessagingPreferences') && $verbose ) {
209     warn <<'END_WARN';
210
211 The "EnhancedMessagingPreferences" syspref is off.
212 Therefore, it is unlikely that this script will actually produce any messages to be sent.
213 To change this, edit the "EnhancedMessagingPreferences" syspref.
214
215 END_WARN
216 }
217 unless ($confirm) {
218      pod2usage(1);
219 }
220 cronlogaction({ info => $command_line_options });
221
222 my %branches = ();
223 if (@branchcodes) {
224     %branches = map { $_ => 1 } @branchcodes;
225 }
226
227 die "--frombranch takes item-homebranch or item-issuebranch only"
228   unless ( $frombranch eq 'item-issuebranch'
229     || $frombranch eq 'item-homebranch' );
230 my $owning_library = ( $frombranch eq 'item-homebranch' ) ? 1 : 0;
231
232 # The fields that will be substituted into <<items.content>>
233 my @item_content_fields = split(/,/,$itemscontent);
234
235 warn 'getting upcoming due issues' if $verbose;
236 my $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( {
237     days_in_advance => $maxdays,
238     owning_library => $owning_library
239  } );
240 warn 'found ' . scalar( @$upcoming_dues ) . ' issues' if $verbose;
241
242 # hash of borrowernumber to number of items upcoming
243 # for patrons wishing digests only.
244 my $upcoming_digest = {};
245 my $due_digest = {};
246
247 my $dbh = C4::Context->dbh();
248 my $sth = $dbh->prepare(<<'END_SQL');
249 SELECT biblio.*, items.*, issues.*
250   FROM issues,items,biblio
251   WHERE items.itemnumber=issues.itemnumber
252     AND biblio.biblionumber=items.biblionumber
253     AND issues.borrowernumber = ?
254     AND issues.itemnumber = ?
255     AND (TO_DAYS(date_due)-TO_DAYS(NOW()) = ?)
256 END_SQL
257
258 my $admin_adress = C4::Context->preference('KohaAdminEmailAddress');
259
260 my @letters;
261 UPCOMINGITEM: foreach my $upcoming ( @$upcoming_dues ) {
262     @letters = ();
263     warn 'examining ' . $upcoming->{'itemnumber'} . ' upcoming due items' if $verbose;
264
265     my $from_address = $upcoming->{branchemail} || $admin_adress;
266
267     my $borrower_preferences;
268     if ( 0 == $upcoming->{'days_until_due'} ) {
269         # This item is due today. Send an 'item due' message.
270         $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $upcoming->{'borrowernumber'},
271                                                                                    message_name   => 'item_due' } );
272         next unless $borrower_preferences;
273         
274         if ( $borrower_preferences->{'wants_digest'} ) {
275             # cache this one to process after we've run through all of the items.
276             if ($digest_per_branch) {
277                 $due_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{email} = $from_address;
278                 $due_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{count}++;
279             } else {
280                 $due_digest->{ $upcoming->{borrowernumber} }->{email} = $from_address;
281                 $due_digest->{ $upcoming->{borrowernumber} }->{count}++;
282             }
283         } else {
284             my $branchcode;
285             if($owning_library) {
286                 $branchcode = $upcoming->{'homebranch'};
287             } else {
288                 $branchcode = $upcoming->{'branchcode'};
289             }
290             # Skip this DUE if we specify list of libraries and this one is not part of it
291             next if (@branchcodes && !$branches{$branchcode});
292
293             my $item = Koha::Items->find( $upcoming->{itemnumber} );
294             my $letter_type = 'DUE';
295             $sth->execute($upcoming->{'borrowernumber'},$upcoming->{'itemnumber'},'0');
296             my $item_info = $sth->fetchrow_hashref();
297             my $title = C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields } );
298
299             ## Get branch info for borrowers home library.
300             foreach my $transport ( keys %{$borrower_preferences->{'transports'}} ) {
301                 next if $transport eq 'itiva';
302                 my $letter = parse_letter( { letter_code    => $letter_type,
303                                       borrowernumber => $upcoming->{'borrowernumber'},
304                                       branchcode     => $branchcode,
305                                       biblionumber   => $item->biblionumber,
306                                       itemnumber     => $upcoming->{'itemnumber'},
307                                       substitute     => {
308                                           'items.content' => $title,
309                                           checkout        => $item_info,
310                                       },
311                                       message_transport_type => $transport,
312                                     } )
313                     or warn "no letter of type '$letter_type' found for borrowernumber ".$upcoming->{'borrowernumber'}.". Please see sample_notices.sql";
314                 push @letters, $letter if $letter;
315             }
316         }
317     } else {
318         $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $upcoming->{'borrowernumber'},
319                                                                                    message_name   => 'advance_notice' } );
320         next UPCOMINGITEM unless $borrower_preferences && exists $borrower_preferences->{'days_in_advance'};
321         next UPCOMINGITEM unless $borrower_preferences->{'days_in_advance'} == $upcoming->{'days_until_due'};
322
323         if ( $borrower_preferences->{'wants_digest'} ) {
324             # cache this one to process after we've run through all of the items.
325             if ($digest_per_branch) {
326                 $upcoming_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{email} = $from_address;
327                 $upcoming_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{count}++;
328             } else {
329                 $upcoming_digest->{ $upcoming->{borrowernumber} }->{email} = $from_address;
330                 $upcoming_digest->{ $upcoming->{borrowernumber} }->{count}++;
331             }
332         } else {
333             my $branchcode;
334             if($owning_library) {
335             $branchcode = $upcoming->{'homebranch'};
336             } else {
337             $branchcode = $upcoming->{'branchcode'};
338             }
339             # Skip this PREDUE if we specify list of libraries and this one is not part of it
340             next if (@branchcodes && !$branches{$branchcode});
341
342             my $item = Koha::Items->find( $upcoming->{itemnumber} );
343             my $letter_type = 'PREDUE';
344             $sth->execute($upcoming->{'borrowernumber'},$upcoming->{'itemnumber'},$borrower_preferences->{'days_in_advance'});
345             my $item_info = $sth->fetchrow_hashref();
346             my $title = C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields } );
347
348             ## Get branch info for borrowers home library.
349             foreach my $transport ( keys %{$borrower_preferences->{'transports'}} ) {
350                 next if $transport eq 'itiva';
351                 my $letter = parse_letter( { letter_code    => $letter_type,
352                                       borrowernumber => $upcoming->{'borrowernumber'},
353                                       branchcode     => $branchcode,
354                                       biblionumber   => $item->biblionumber,
355                                       itemnumber     => $upcoming->{'itemnumber'},
356                                       substitute     => {
357                                           'items.content' => $title,
358                                           checkout        => $item_info,
359                                       },
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 $digest_query = 'SELECT biblio.*, items.*, issues.*
392   FROM issues,items,biblio
393   WHERE items.itemnumber=issues.itemnumber
394     AND biblio.biblionumber=items.biblionumber
395     AND issues.borrowernumber = ?
396     AND (TO_DAYS(date_due)-TO_DAYS(NOW()) = ?)';
397
398 my $sth_digest = $dbh->prepare($digest_query);
399
400 if ($digest_per_branch) {
401     while (my ($branchcode, $digests) = each %$upcoming_digest) {
402         send_digests({
403             sth => $dbh->prepare( $digest_query . " AND issues.branchcode = '" . $branchcode . "'"),
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 => $dbh->prepare( $digest_query . " AND issues.branchcode = '" . $branchcode . "'" ),
422             digests => $digests,
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         ( $params->{itemnumbers} ? ( loops => { items => $params->{itemnumbers} } ) : () ),
502         message_transport_type => $params->{message_transport_type},
503     );
504 }
505
506 =head2 get_branch_info
507
508 =cut
509
510 sub get_branch_info {
511     my ( $borrowernumber ) = @_;
512
513     ## Get branch info for borrowers home library.
514     my $patron = Koha::Patrons->find( $borrowernumber );
515     my $branch = $patron->library->unblessed;
516     my %branch_info;
517     foreach my $key( keys %$branch ) {
518         $branch_info{"branches.$key"} = $branch->{$key};
519     }
520
521     return %branch_info;
522 }
523
524 =head2 send_digests
525
526     send_digests({
527         digests => ...,
528         sth => ...,
529         letter_code => ...,
530         get_item_info => ...,
531     })
532
533 Enqueue digested letters (or print them if -n was passed at command line).
534
535 Parameters:
536
537 =over 4
538
539 =item C<$digests>
540
541 Reference to the array of digested messages.
542
543 =item C<$sth>
544
545 Prepared statement handle for fetching overdue issues.
546
547 =item C<$letter_code>
548
549 String that denote the letter code.
550
551 =item C<$get_item_info>
552
553 Subroutine for executing prepared statement.  Takes parameters $sth,
554 $borrowernumber and $borrower_parameters and return a generator
555 function that produce the matching rows.
556
557 =back
558
559 =cut
560
561 sub send_digests {
562     my $params = shift;
563
564     PATRON: while ( my ( $borrowernumber, $digest ) = each %{$params->{digests}} ) {
565         @letters = ();
566         my $count = $digest->{count};
567         my $from_address = $digest->{email};
568
569         my %branch_info;
570         my $branchcode;
571
572         if (defined($params->{branchcode})) {
573             %branch_info = ();
574             $branchcode = $params->{branchcode};
575         } else {
576             ## Get branch info for borrowers home library.
577             %branch_info = get_branch_info( $borrowernumber );
578             $branchcode = $branch_info{'branches.branchcode'};
579         }
580
581         my $borrower_preferences =
582             C4::Members::Messaging::GetMessagingPreferences(
583                 {
584                     borrowernumber => $borrowernumber,
585                     message_name   => $params->{message_name}
586                 }
587             );
588
589         next PATRON unless $borrower_preferences; # how could this happen?
590
591         my $next_item_info = $params->{get_item_info}->({
592             sth => $params->{sth},
593             borrowernumber => $borrowernumber,
594             borrower_preferences => $borrower_preferences
595         });
596         my $titles = "";
597         my @itemnumbers;
598         my @checkouts;
599         while ( my $item_info = $next_item_info->()) {
600             push @itemnumbers, $item_info->{itemnumber};
601             push( @checkouts, $item_info );
602             $titles .= C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields } );
603         }
604
605         foreach my $transport ( keys %{ $borrower_preferences->{'transports'} } ) {
606             next if $transport eq 'itiva';
607             my $letter = parse_letter(
608                 {
609                     letter_code    => $params->{letter_code},
610                     borrowernumber => $borrowernumber,
611                     checkouts      => \@checkouts,
612                     substitute     => {
613                         count           => $count,
614                         'items.content' => $titles,
615                         checkouts       => \@checkouts,
616                         %branch_info
617                     },
618                     itemnumbers    => \@itemnumbers,
619                     branchcode     => $branchcode,
620                     message_transport_type => $transport
621                 }
622             );
623             unless ( $letter ){
624                 warn "no letter of type '$params->{letter_type}' found for borrowernumber $borrowernumber. Please see sample_notices.sql";
625                 next;
626             }
627             push @letters, $letter if $letter;
628         }
629
630         if ( @letters ) {
631             if ($nomail) {
632                 for my $letter ( @letters ) {
633                     local $, = "\f";
634                     print $letter->{'content'};
635                 }
636             }
637             else {
638                 for my $letter ( @letters ) {
639                     C4::Letters::EnqueueLetter( { letter                 => $letter,
640                                                   borrowernumber         => $borrowernumber,
641                                                   from_address           => $from_address,
642                                                   message_transport_type => $letter->{message_transport_type} } );
643                 }
644             }
645         }
646     }
647 }
648
649 cronlogaction({ action => 'End', info => "COMPLETED" });
650
651 1;
652
653 __END__