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