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