Bug 20457: Overdue and pre-overdue cronjobs not skipping phone notices
[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 GetOptions(
189             'help|?'         => \$help,
190             'man'            => \$man,
191             'library=s'      => \@branchcodes,
192             'frombranch=s'   => \$frombranch,
193             'c'              => \$confirm,
194             'n'              => \$nomail,
195             'm:i'            => \$maxdays,
196             'v'              => \$verbose,
197             'digest-per-branch' => \$digest_per_branch,
198             'itemscontent=s' => \$itemscontent,
199        )or pod2usage(2);
200 pod2usage(1) if $help;
201 pod2usage( -verbose => 2 ) if $man;
202
203 # Since advance notice options are not visible in the web-interface
204 # unless EnhancedMessagingPreferences is on, let the user know that
205 # this script probably isn't going to do much
206 if ( ! C4::Context->preference('EnhancedMessagingPreferences') && $verbose ) {
207     warn <<'END_WARN';
208
209 The "EnhancedMessagingPreferences" syspref is off.
210 Therefore, it is unlikely that this script will actually produce any messages to be sent.
211 To change this, edit the "EnhancedMessagingPreferences" syspref.
212
213 END_WARN
214 }
215 unless ($confirm) {
216      pod2usage(1);
217 }
218 cronlogaction();
219
220 my %branches = ();
221 if (@branchcodes) {
222     %branches = map { $_ => 1 } @branchcodes;
223 }
224
225 die "--frombranch takes item-homebranch or item-issuebranch only"
226   unless ( $frombranch eq 'item-issuebranch'
227     || $frombranch eq 'item-homebranch' );
228 my $owning_library = ( $frombranch eq 'item-homebranch' ) ? 1 : 0;
229
230 # The fields that will be substituted into <<items.content>>
231 my @item_content_fields = split(/,/,$itemscontent);
232
233 warn 'getting upcoming due issues' if $verbose;
234 my $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( {
235     days_in_advance => $maxdays,
236     owning_library => $owning_library
237  } );
238 warn 'found ' . scalar( @$upcoming_dues ) . ' issues' if $verbose;
239
240 # hash of borrowernumber to number of items upcoming
241 # for patrons wishing digests only.
242 my $upcoming_digest = {};
243 my $due_digest = {};
244
245 my $dbh = C4::Context->dbh();
246 my $sth = $dbh->prepare(<<'END_SQL');
247 SELECT biblio.*, items.*, issues.*
248   FROM issues,items,biblio
249   WHERE items.itemnumber=issues.itemnumber
250     AND biblio.biblionumber=items.biblionumber
251     AND issues.borrowernumber = ?
252     AND issues.itemnumber = ?
253     AND (TO_DAYS(date_due)-TO_DAYS(NOW()) = ?)
254 END_SQL
255
256 my $admin_adress = C4::Context->preference('KohaAdminEmailAddress');
257
258 my @letters;
259 UPCOMINGITEM: foreach my $upcoming ( @$upcoming_dues ) {
260     @letters = ();
261     warn 'examining ' . $upcoming->{'itemnumber'} . ' upcoming due items' if $verbose;
262
263     my $from_address = $upcoming->{branchemail} || $admin_adress;
264
265     my $borrower_preferences;
266     if ( 0 == $upcoming->{'days_until_due'} ) {
267         # This item is due today. Send an 'item due' message.
268         $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $upcoming->{'borrowernumber'},
269                                                                                    message_name   => 'item_due' } );
270         next unless $borrower_preferences;
271         
272         if ( $borrower_preferences->{'wants_digest'} ) {
273             # cache this one to process after we've run through all of the items.
274             if ($digest_per_branch) {
275                 $due_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{email} = $from_address;
276                 $due_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{count}++;
277             } else {
278                 $due_digest->{ $upcoming->{borrowernumber} }->{email} = $from_address;
279                 $due_digest->{ $upcoming->{borrowernumber} }->{count}++;
280             }
281         } else {
282             my $branchcode;
283             if($owning_library) {
284                 $branchcode = $upcoming->{'homebranch'};
285             } else {
286                 $branchcode = $upcoming->{'branchcode'};
287             }
288             # Skip this DUE if we specify list of libraries and this one is not part of it
289             next if (@branchcodes && !$branches{$branchcode});
290
291             my $item = Koha::Items->find( $upcoming->{itemnumber} );
292             my $letter_type = 'DUE';
293             $sth->execute($upcoming->{'borrowernumber'},$upcoming->{'itemnumber'},'0');
294             my $titles = "";
295             while ( my $item_info = $sth->fetchrow_hashref()) {
296                 $titles .= C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields } );
297             }
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     => { 'items.content' => $titles },
308                                       message_transport_type => $transport,
309                                     } )
310                     or warn "no letter of type '$letter_type' found for borrowernumber ".$upcoming->{'borrowernumber'}.". Please see sample_notices.sql";
311                 push @letters, $letter if $letter;
312             }
313         }
314     } else {
315         $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $upcoming->{'borrowernumber'},
316                                                                                    message_name   => 'advance_notice' } );
317         next UPCOMINGITEM unless $borrower_preferences && exists $borrower_preferences->{'days_in_advance'};
318         next UPCOMINGITEM unless $borrower_preferences->{'days_in_advance'} == $upcoming->{'days_until_due'};
319
320         if ( $borrower_preferences->{'wants_digest'} ) {
321             # cache this one to process after we've run through all of the items.
322             if ($digest_per_branch) {
323                 $upcoming_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{email} = $from_address;
324                 $upcoming_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{count}++;
325             } else {
326                 $upcoming_digest->{ $upcoming->{borrowernumber} }->{email} = $from_address;
327                 $upcoming_digest->{ $upcoming->{borrowernumber} }->{count}++;
328             }
329         } else {
330             my $branchcode;
331             if($owning_library) {
332             $branchcode = $upcoming->{'homebranch'};
333             } else {
334             $branchcode = $upcoming->{'branchcode'};
335             }
336             # Skip this PREDUE if we specify list of libraries and this one is not part of it
337             next if (@branchcodes && !$branches{$branchcode});
338
339             my $item = Koha::Items->find( $upcoming->{itemnumber} );
340             my $letter_type = 'PREDUE';
341             $sth->execute($upcoming->{'borrowernumber'},$upcoming->{'itemnumber'},$borrower_preferences->{'days_in_advance'});
342             my $titles = "";
343             while ( my $item_info = $sth->fetchrow_hashref()) {
344                 $titles .= C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields } );
345             }
346
347             ## Get branch info for borrowers home library.
348             foreach my $transport ( keys %{$borrower_preferences->{'transports'}} ) {
349                 next if $transport eq 'itiva';
350                 my $letter = parse_letter( { letter_code    => $letter_type,
351                                       borrowernumber => $upcoming->{'borrowernumber'},
352                                       branchcode     => $branchcode,
353                                       biblionumber   => $item->biblionumber,
354                                       itemnumber     => $upcoming->{'itemnumber'},
355                                       substitute     => { 'items.content' => $titles },
356                                       message_transport_type => $transport,
357                                     } )
358                     or warn "no letter of type '$letter_type' found for borrowernumber ".$upcoming->{'borrowernumber'}.". Please see sample_notices.sql";
359                 push @letters, $letter if $letter;
360             }
361         }
362     }
363
364     # If we have prepared a letter, send it.
365     if ( @letters ) {
366       if ($nomail) {
367         for my $letter ( @letters ) {
368             local $, = "\f";
369             print $letter->{'content'}."\n";
370         }
371       }
372       else {
373         for my $letter ( @letters ) {
374             C4::Letters::EnqueueLetter( { letter                 => $letter,
375                                           borrowernumber         => $upcoming->{'borrowernumber'},
376                                           from_address           => $from_address,
377                                           message_transport_type => $letter->{message_transport_type} } );
378         }
379       }
380     }
381 }
382
383
384
385 # Now, run through all the people that want digests and send them
386
387 my $sth_digest = $dbh->prepare(<<'END_SQL');
388 SELECT biblio.*, items.*, issues.*
389   FROM issues,items,biblio
390   WHERE items.itemnumber=issues.itemnumber
391     AND biblio.biblionumber=items.biblionumber
392     AND issues.borrowernumber = ?
393     AND (TO_DAYS(date_due)-TO_DAYS(NOW()) = ?)
394 END_SQL
395
396 if ($digest_per_branch) {
397     while (my ($branchcode, $digests) = each %$upcoming_digest) {
398         send_digests({
399             sth => $sth_digest,
400             digests => $digests,
401             letter_code => 'PREDUEDGST',
402             message_name => 'advance_notice',
403             branchcode => $branchcode,
404             get_item_info => sub {
405                 my $params = shift;
406                 $params->{sth}->execute($params->{borrowernumber},
407                                         $params->{borrower_preferences}->{'days_in_advance'});
408                 return sub {
409                     $params->{sth}->fetchrow_hashref;
410                 };
411             }
412         });
413     }
414
415     while (my ($branchcode, $digests) = each %$due_digest) {
416         send_digests({
417             sth => $sth_digest,
418             digests => $due_digest,
419             letter_code => 'DUEDGST',
420             branchcode => $branchcode,
421             message_name => 'item_due',
422             get_item_info => sub {
423                 my $params = shift;
424                 $params->{sth}->execute($params->{borrowernumber}, 0);
425                 return sub {
426                     $params->{sth}->fetchrow_hashref;
427                 };
428             }
429         });
430     }
431 } else {
432     send_digests({
433         sth => $sth_digest,
434         digests => $upcoming_digest,
435         letter_code => 'PREDUEDGST',
436         message_name => 'advance_notice',
437         get_item_info => sub {
438             my $params = shift;
439             $params->{sth}->execute($params->{borrowernumber},
440                                     $params->{borrower_preferences}->{'days_in_advance'});
441             return sub {
442                 $params->{sth}->fetchrow_hashref;
443             };
444         }
445     });
446
447     send_digests({
448         sth => $sth_digest,
449         digests => $due_digest,
450         letter_code => 'DUEDGST',
451         message_name => 'item_due',
452         get_item_info => sub {
453             my $params = shift;
454             $params->{sth}->execute($params->{borrowernumber}, 0);
455             return sub {
456                 $params->{sth}->fetchrow_hashref;
457             };
458         }
459     });
460 }
461
462 =head1 METHODS
463
464 =head2 parse_letter
465
466 =cut
467
468 sub parse_letter {
469     my $params = shift;
470
471     foreach my $required ( qw( letter_code borrowernumber ) ) {
472         return unless exists $params->{$required};
473     }
474     my $patron = Koha::Patrons->find( $params->{borrowernumber} );
475
476     my %table_params = ( 'borrowers' => $params->{'borrowernumber'} );
477
478     if ( my $p = $params->{'branchcode'} ) {
479         $table_params{'branches'} = $p;
480     }
481     if ( my $p = $params->{'itemnumber'} ) {
482         $table_params{'issues'} = $p;
483         $table_params{'items'} = $p;
484     }
485     if ( my $p = $params->{'biblionumber'} ) {
486         $table_params{'biblio'} = $p;
487         $table_params{'biblioitems'} = $p;
488     }
489
490     return C4::Letters::GetPreparedLetter (
491         module => 'circulation',
492         letter_code => $params->{'letter_code'},
493         branchcode => $table_params{'branches'},
494         lang => $patron->lang,
495         substitute => $params->{'substitute'},
496         tables     => \%table_params,
497         ( $params->{itemnumbers} ? ( loops => { items => $params->{itemnumbers} } ) : () ),
498         message_transport_type => $params->{message_transport_type},
499     );
500 }
501
502 =head2 get_branch_info
503
504 =cut
505
506 sub get_branch_info {
507     my ( $borrowernumber ) = @_;
508
509     ## Get branch info for borrowers home library.
510     my $patron = Koha::Patrons->find( $borrowernumber );
511     my $branch = $patron->library->unblessed;
512     my %branch_info;
513     foreach my $key( keys %$branch ) {
514         $branch_info{"branches.$key"} = $branch->{$key};
515     }
516
517     return %branch_info;
518 }
519
520 =head2 send_digests
521
522     send_digests({
523         digests => ...,
524         sth => ...,
525         letter_code => ...,
526         get_item_info => ...,
527     })
528
529 Enqueue digested letters (or print them if -n was passed at command line).
530
531 Parameters:
532
533 =over 4
534
535 =item C<$digests>
536
537 Reference to the array of digested messages.
538
539 =item C<$sth>
540
541 Prepared statement handle for fetching overdue issues.
542
543 =item C<$letter_code>
544
545 String that denote the letter code.
546
547 =item C<$get_item_info>
548
549 Subroutine for executing prepared statement.  Takes parameters $sth,
550 $borrowernumber and $borrower_parameters and return a generator
551 function that produce the matching rows.
552
553 =back
554
555 =cut
556
557 sub send_digests {
558     my $params = shift;
559
560     PATRON: while ( my ( $borrowernumber, $digest ) = each %{$params->{digests}} ) {
561         @letters = ();
562         my $count = $digest->{count};
563         my $from_address = $digest->{email};
564
565         my %branch_info;
566         my $branchcode;
567
568         if (defined($params->{branchcode})) {
569             %branch_info = ();
570             $branchcode = $params->{branchcode};
571         } else {
572             ## Get branch info for borrowers home library.
573             %branch_info = get_branch_info( $borrowernumber );
574             $branchcode = $branch_info{'branches.branchcode'};
575         }
576
577         my $borrower_preferences =
578             C4::Members::Messaging::GetMessagingPreferences(
579                 {
580                     borrowernumber => $borrowernumber,
581                     message_name   => $params->{message_name}
582                 }
583             );
584
585         next PATRON unless $borrower_preferences; # how could this happen?
586
587         my $next_item_info = $params->{get_item_info}->({
588             sth => $params->{sth},
589             borrowernumber => $borrowernumber,
590             borrower_preferences => $borrower_preferences
591         });
592         my $titles = "";
593         my @itemnumbers;
594         while ( my $item_info = $next_item_info->()) {
595             push @itemnumbers, $item_info->{itemnumber};
596             $titles .= C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields } );
597         }
598
599         foreach my $transport ( keys %{ $borrower_preferences->{'transports'} } ) {
600             next if $transport eq 'itiva';
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                     itemnumbers    => \@itemnumbers,
611                     branchcode     => $branchcode,
612                     message_transport_type => $transport
613                 }
614             );
615             unless ( $letter ){
616                 warn "no letter of type '$params->{letter_type}' found for borrowernumber $borrowernumber. Please see sample_notices.sql";
617                 next;
618             }
619             push @letters, $letter if $letter;
620         }
621
622         if ( @letters ) {
623             if ($nomail) {
624                 for my $letter ( @letters ) {
625                     local $, = "\f";
626                     print $letter->{'content'};
627                 }
628             }
629             else {
630                 for my $letter ( @letters ) {
631                     C4::Letters::EnqueueLetter( { letter                 => $letter,
632                                                   borrowernumber         => $borrowernumber,
633                                                   from_address           => $from_address,
634                                                   message_transport_type => $letter->{message_transport_type} } );
635                 }
636             }
637         }
638     }
639 }
640
641
642 1;
643
644 __END__