Bug 31540: Exclude expired holds from the reminder job
[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                 my $letter = parse_letter( { letter_code    => $letter_type,
302                                       borrowernumber => $upcoming->{'borrowernumber'},
303                                       branchcode     => $branchcode,
304                                       biblionumber   => $item->biblionumber,
305                                       itemnumber     => $upcoming->{'itemnumber'},
306                                       substitute     => { 'items.content' => $titles },
307                                       message_transport_type => $transport,
308                                     } )
309                     or warn "no letter of type '$letter_type' found for borrowernumber ".$upcoming->{'borrowernumber'}.". Please see sample_notices.sql";
310                 push @letters, $letter if $letter;
311             }
312         }
313     } else {
314         $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $upcoming->{'borrowernumber'},
315                                                                                    message_name   => 'advance_notice' } );
316         next UPCOMINGITEM unless $borrower_preferences && exists $borrower_preferences->{'days_in_advance'};
317         next UPCOMINGITEM unless $borrower_preferences->{'days_in_advance'} == $upcoming->{'days_until_due'};
318
319         if ( $borrower_preferences->{'wants_digest'} ) {
320             # cache this one to process after we've run through all of the items.
321             if ($digest_per_branch) {
322                 $upcoming_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{email} = $from_address;
323                 $upcoming_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{count}++;
324             } else {
325                 $upcoming_digest->{ $upcoming->{borrowernumber} }->{email} = $from_address;
326                 $upcoming_digest->{ $upcoming->{borrowernumber} }->{count}++;
327             }
328         } else {
329             my $branchcode;
330             if($owning_library) {
331             $branchcode = $upcoming->{'homebranch'};
332             } else {
333             $branchcode = $upcoming->{'branchcode'};
334             }
335             # Skip this PREDUE if we specify list of libraries and this one is not part of it
336             next if (@branchcodes && !$branches{$branchcode});
337
338             my $item = Koha::Items->find( $upcoming->{itemnumber} );
339             my $letter_type = 'PREDUE';
340             $sth->execute($upcoming->{'borrowernumber'},$upcoming->{'itemnumber'},$borrower_preferences->{'days_in_advance'});
341             my $titles = "";
342             while ( my $item_info = $sth->fetchrow_hashref()) {
343                 $titles .= C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields } );
344             }
345
346             ## Get branch info for borrowers home library.
347             foreach my $transport ( keys %{$borrower_preferences->{'transports'}} ) {
348                 my $letter = parse_letter( { letter_code    => $letter_type,
349                                       borrowernumber => $upcoming->{'borrowernumber'},
350                                       branchcode     => $branchcode,
351                                       biblionumber   => $item->biblionumber,
352                                       itemnumber     => $upcoming->{'itemnumber'},
353                                       substitute     => { 'items.content' => $titles },
354                                       message_transport_type => $transport,
355                                     } )
356                     or warn "no letter of type '$letter_type' found for borrowernumber ".$upcoming->{'borrowernumber'}.". Please see sample_notices.sql";
357                 push @letters, $letter if $letter;
358             }
359         }
360     }
361
362     # If we have prepared a letter, send it.
363     if ( @letters ) {
364       if ($nomail) {
365         for my $letter ( @letters ) {
366             local $, = "\f";
367             print $letter->{'content'}."\n";
368         }
369       }
370       else {
371         for my $letter ( @letters ) {
372             C4::Letters::EnqueueLetter( { letter                 => $letter,
373                                           borrowernumber         => $upcoming->{'borrowernumber'},
374                                           from_address           => $from_address,
375                                           message_transport_type => $letter->{message_transport_type} } );
376         }
377       }
378     }
379 }
380
381
382
383 # Now, run through all the people that want digests and send them
384
385 my $sth_digest = $dbh->prepare(<<'END_SQL');
386 SELECT biblio.*, items.*, issues.*
387   FROM issues,items,biblio
388   WHERE items.itemnumber=issues.itemnumber
389     AND biblio.biblionumber=items.biblionumber
390     AND issues.borrowernumber = ?
391     AND (TO_DAYS(date_due)-TO_DAYS(NOW()) = ?)
392 END_SQL
393
394 if ($digest_per_branch) {
395     while (my ($branchcode, $digests) = each %$upcoming_digest) {
396         send_digests({
397             sth => $sth_digest,
398             digests => $digests,
399             letter_code => 'PREDUEDGST',
400             message_name => 'advance_notice',
401             branchcode => $branchcode,
402             get_item_info => sub {
403                 my $params = shift;
404                 $params->{sth}->execute($params->{borrowernumber},
405                                         $params->{borrower_preferences}->{'days_in_advance'});
406                 return sub {
407                     $params->{sth}->fetchrow_hashref;
408                 };
409             }
410         });
411     }
412
413     while (my ($branchcode, $digests) = each %$due_digest) {
414         send_digests({
415             sth => $sth_digest,
416             digests => $due_digest,
417             letter_code => 'DUEDGST',
418             branchcode => $branchcode,
419             message_name => 'item_due',
420             get_item_info => sub {
421                 my $params = shift;
422                 $params->{sth}->execute($params->{borrowernumber}, 0);
423                 return sub {
424                     $params->{sth}->fetchrow_hashref;
425                 };
426             }
427         });
428     }
429 } else {
430     send_digests({
431         sth => $sth_digest,
432         digests => $upcoming_digest,
433         letter_code => 'PREDUEDGST',
434         message_name => 'advance_notice',
435         get_item_info => sub {
436             my $params = shift;
437             $params->{sth}->execute($params->{borrowernumber},
438                                     $params->{borrower_preferences}->{'days_in_advance'});
439             return sub {
440                 $params->{sth}->fetchrow_hashref;
441             };
442         }
443     });
444
445     send_digests({
446         sth => $sth_digest,
447         digests => $due_digest,
448         letter_code => 'DUEDGST',
449         message_name => 'item_due',
450         get_item_info => sub {
451             my $params = shift;
452             $params->{sth}->execute($params->{borrowernumber}, 0);
453             return sub {
454                 $params->{sth}->fetchrow_hashref;
455             };
456         }
457     });
458 }
459
460 =head1 METHODS
461
462 =head2 parse_letter
463
464 =cut
465
466 sub parse_letter {
467     my $params = shift;
468
469     foreach my $required ( qw( letter_code borrowernumber ) ) {
470         return unless exists $params->{$required};
471     }
472     my $patron = Koha::Patrons->find( $params->{borrowernumber} );
473
474     my %table_params = ( 'borrowers' => $params->{'borrowernumber'} );
475
476     if ( my $p = $params->{'branchcode'} ) {
477         $table_params{'branches'} = $p;
478     }
479     if ( my $p = $params->{'itemnumber'} ) {
480         $table_params{'issues'} = $p;
481         $table_params{'items'} = $p;
482     }
483     if ( my $p = $params->{'biblionumber'} ) {
484         $table_params{'biblio'} = $p;
485         $table_params{'biblioitems'} = $p;
486     }
487
488     return C4::Letters::GetPreparedLetter (
489         module => 'circulation',
490         letter_code => $params->{'letter_code'},
491         branchcode => $table_params{'branches'},
492         lang => $patron->lang,
493         substitute => $params->{'substitute'},
494         tables     => \%table_params,
495         ( $params->{itemnumbers} ? ( loops => { items => $params->{itemnumbers} } ) : () ),
496         message_transport_type => $params->{message_transport_type},
497     );
498 }
499
500 =head2 get_branch_info
501
502 =cut
503
504 sub get_branch_info {
505     my ( $borrowernumber ) = @_;
506
507     ## Get branch info for borrowers home library.
508     my $patron = Koha::Patrons->find( $borrowernumber );
509     my $branch = $patron->library->unblessed;
510     my %branch_info;
511     foreach my $key( keys %$branch ) {
512         $branch_info{"branches.$key"} = $branch->{$key};
513     }
514
515     return %branch_info;
516 }
517
518 =head2 send_digests
519
520     send_digests({
521         digests => ...,
522         sth => ...,
523         letter_code => ...,
524         get_item_info => ...,
525     })
526
527 Enqueue digested letters (or print them if -n was passed at command line).
528
529 Parameters:
530
531 =over 4
532
533 =item C<$digests>
534
535 Reference to the array of digested messages.
536
537 =item C<$sth>
538
539 Prepared statement handle for fetching overdue issues.
540
541 =item C<$letter_code>
542
543 String that denote the letter code.
544
545 =item C<$get_item_info>
546
547 Subroutine for executing prepared statement.  Takes parameters $sth,
548 $borrowernumber and $borrower_parameters and return a generator
549 function that produce the matching rows.
550
551 =back
552
553 =cut
554
555 sub send_digests {
556     my $params = shift;
557
558     PATRON: while ( my ( $borrowernumber, $digest ) = each %{$params->{digests}} ) {
559         @letters = ();
560         my $count = $digest->{count};
561         my $from_address = $digest->{email};
562
563         my %branch_info;
564         my $branchcode;
565
566         if (defined($params->{branchcode})) {
567             %branch_info = ();
568             $branchcode = $params->{branchcode};
569         } else {
570             ## Get branch info for borrowers home library.
571             %branch_info = get_branch_info( $borrowernumber );
572             $branchcode = $branch_info{'branches.branchcode'};
573         }
574
575         my $borrower_preferences =
576             C4::Members::Messaging::GetMessagingPreferences(
577                 {
578                     borrowernumber => $borrowernumber,
579                     message_name   => $params->{message_name}
580                 }
581             );
582
583         next PATRON unless $borrower_preferences; # how could this happen?
584
585         my $next_item_info = $params->{get_item_info}->({
586             sth => $params->{sth},
587             borrowernumber => $borrowernumber,
588             borrower_preferences => $borrower_preferences
589         });
590         my $titles = "";
591         my @itemnumbers;
592         while ( my $item_info = $next_item_info->()) {
593             push @itemnumbers, $item_info->{itemnumber};
594             $titles .= C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields } );
595         }
596
597         foreach my $transport ( keys %{ $borrower_preferences->{'transports'} } ) {
598             my $letter = parse_letter(
599                 {
600                     letter_code    => $params->{letter_code},
601                     borrowernumber => $borrowernumber,
602                     substitute     => {
603                         count           => $count,
604                         'items.content' => $titles,
605                         %branch_info
606                     },
607                     itemnumbers    => \@itemnumbers,
608                     branchcode     => $branchcode,
609                     message_transport_type => $transport
610                 }
611             );
612             unless ( $letter ){
613                 warn "no letter of type '$params->{letter_type}' found for borrowernumber $borrowernumber. Please see sample_notices.sql";
614                 next;
615             }
616             push @letters, $letter if $letter;
617         }
618
619         if ( @letters ) {
620             if ($nomail) {
621                 for my $letter ( @letters ) {
622                     local $, = "\f";
623                     print $letter->{'content'};
624                 }
625             }
626             else {
627                 for my $letter ( @letters ) {
628                     C4::Letters::EnqueueLetter( { letter                 => $letter,
629                                                   borrowernumber         => $borrowernumber,
630                                                   from_address           => $from_address,
631                                                   message_transport_type => $letter->{message_transport_type} } );
632                 }
633             }
634         }
635     }
636 }
637
638
639 1;
640
641 __END__