Bug 21886: (QA follow-up) Fix QA script errors
[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<--owning>
135
136 Use the address information from the item homebranch library instead of the issuing library.
137
138 =back
139
140 =head1 DESCRIPTION
141
142 This script is designed to alert patrons when items are due, or coming due
143
144 =head2 Configuration
145
146 This script pays attention to the advanced notice configuration
147 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
148 section of Koha is available in the Koha manual.
149
150 =head2 Outgoing emails
151
152 Typically, messages are prepared for each patron with due
153 items, and who have selected (or the library has elected for them) Advance or Due notices.
154
155 These emails are staged in the outgoing message queue, as are messages
156 produced by other features of Koha. This message queue must be
157 processed regularly by the
158 F<misc/cronjobs/process_message_queue.pl> program.
159
160 In the event that the C<-n> flag is passed to this program, no emails
161 are sent. Instead, messages are sent on standard output from this
162 program. They may be redirected to a file if desired.
163
164 =head2 Templates
165
166 Templates can contain variables enclosed in double angle brackets like
167 E<lt>E<lt>thisE<gt>E<gt>. Those variables will be replaced with values
168 specific to the overdue items or relevant patron. Available variables
169 are:
170
171 =over
172
173 =item E<lt>E<lt>items.contentE<gt>E<gt>
174
175 one line for each item, each line containing a tab separated list of
176 date due, title, author, barcode
177
178 =item E<lt>E<lt>borrowers.*E<gt>E<gt>
179
180 any field from the borrowers table
181
182 =item E<lt>E<lt>branches.*E<gt>E<gt>
183
184 any field from the branches table
185
186 =back
187
188 =head1 SEE ALSO
189
190 The F<misc/cronjobs/overdue_notices.pl> program allows you to send
191 messages to patrons when their messages are overdue.
192
193 =cut
194
195 binmode( STDOUT, ':encoding(UTF-8)' );
196
197 # These are defaults for command line options.
198 my $confirm;                                                        # -c: Confirm that the user has read and configured this script.
199 my $nomail;                                                         # -n: No mail. Will not send any emails.
200 my $mindays     = 0;                                                # -m: Maximum number of days in advance to send notices
201 my $maxdays     = 30;                                               # -e: the End of the time period
202 my $verbose     = 0;                                                # -v: verbose
203 my $digest_per_branch = 0;                                          # -digest-per-branch: Prepare and send digests per branch
204 my @branchcodes; # Branch(es) passed as parameter
205 my $owning_library = 0;
206 my $itemscontent = join(',',qw( date_due title author barcode ));
207
208 my $help    = 0;
209 my $man     = 0;
210
211 GetOptions(
212             'help|?'         => \$help,
213             'man'            => \$man,
214             'library=s'      => \@branchcodes,
215             'owning'         => \$owning_library,
216             'c'              => \$confirm,
217             'n'              => \$nomail,
218             'm:i'            => \$maxdays,
219             'v'              => \$verbose,
220             'digest-per-branch' => \$digest_per_branch,
221             'itemscontent=s' => \$itemscontent,
222        )or pod2usage(2);
223 pod2usage(1) if $help;
224 pod2usage( -verbose => 2 ) if $man;
225
226 # Since advance notice options are not visible in the web-interface
227 # unless EnhancedMessagingPreferences is on, let the user know that
228 # this script probably isn't going to do much
229 if ( ! C4::Context->preference('EnhancedMessagingPreferences') ) {
230     warn <<'END_WARN';
231
232 The "EnhancedMessagingPreferences" syspref is off.
233 Therefore, it is unlikely that this script will actually produce any messages to be sent.
234 To change this, edit the "EnhancedMessagingPreferences" syspref.
235
236 END_WARN
237 }
238 unless ($confirm) {
239      pod2usage(1);
240 }
241 cronlogaction();
242
243 my %branches = {};
244 if (@branchcodes) {
245     %branches = map { $_ => 1 } @branchcodes;
246 }
247
248 # The fields that will be substituted into <<items.content>>
249 my @item_content_fields = split(/,/,$itemscontent);
250
251 warn 'getting upcoming due issues' if $verbose;
252 my $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( {
253     days_in_advance => $maxdays,
254     owning_library => $owning_library
255  } );
256 warn 'found ' . scalar( @$upcoming_dues ) . ' issues' if $verbose;
257
258 # hash of borrowernumber to number of items upcoming
259 # for patrons wishing digests only.
260 my $upcoming_digest = {};
261 my $due_digest = {};
262
263 my $dbh = C4::Context->dbh();
264 my $sth = $dbh->prepare(<<'END_SQL');
265 SELECT biblio.*, items.*, issues.*
266   FROM issues,items,biblio
267   WHERE items.itemnumber=issues.itemnumber
268     AND biblio.biblionumber=items.biblionumber
269     AND issues.borrowernumber = ?
270     AND issues.itemnumber = ?
271     AND (TO_DAYS(date_due)-TO_DAYS(NOW()) = ?)
272 END_SQL
273
274 my $admin_adress = C4::Context->preference('KohaAdminEmailAddress');
275
276 my @letters;
277 UPCOMINGITEM: foreach my $upcoming ( @$upcoming_dues ) {
278     @letters = ();
279     warn 'examining ' . $upcoming->{'itemnumber'} . ' upcoming due items' if $verbose;
280
281     my $from_address = $upcoming->{branchemail} || $admin_adress;
282
283     my $borrower_preferences;
284     if ( 0 == $upcoming->{'days_until_due'} ) {
285         # This item is due today. Send an 'item due' message.
286         $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $upcoming->{'borrowernumber'},
287                                                                                    message_name   => 'item_due' } );
288         next unless $borrower_preferences;
289         
290         if ( $borrower_preferences->{'wants_digest'} ) {
291             # cache this one to process after we've run through all of the items.
292             if ($digest_per_branch) {
293                 $due_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{email} = $from_address;
294                 $due_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{count}++;
295             } else {
296                 $due_digest->{ $upcoming->{borrowernumber} }->{email} = $from_address;
297                 $due_digest->{ $upcoming->{borrowernumber} }->{count}++;
298             }
299         } else {
300         my $branchcode;
301         if($owning_library) {
302         $branchcode = $upcoming->{'homebranch'};
303         } else {
304         $branchcode = $upcoming->{'branchcode'};
305         }
306         # Skip this DUE if we specify list of libraries and this one is not part of it
307         next if (@branchcodes && !$branches{$branchcode});
308
309             my $item = Koha::Items->find( $upcoming->{itemnumber} );
310             my $letter_type = 'DUE';
311             $sth->execute($upcoming->{'borrowernumber'},$upcoming->{'itemnumber'},'0');
312             my $titles = "";
313             while ( my $item_info = $sth->fetchrow_hashref()) {
314                 $titles .= C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields } );
315             }
316
317             ## Get branch info for borrowers home library.
318             foreach my $transport ( keys %{$borrower_preferences->{'transports'}} ) {
319                 my $letter = parse_letter( { letter_code    => $letter_type,
320                                       borrowernumber => $upcoming->{'borrowernumber'},
321                                       branchcode     => $branchcode,
322                                       biblionumber   => $item->biblionumber,
323                                       itemnumber     => $upcoming->{'itemnumber'},
324                                       substitute     => { 'items.content' => $titles },
325                                       message_transport_type => $transport,
326                                     } )
327                     or warn "no letter of type '$letter_type' found for borrowernumber ".$upcoming->{'borrowernumber'}.". Please see sample_notices.sql";
328                 push @letters, $letter if $letter;
329             }
330         }
331     } else {
332         $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $upcoming->{'borrowernumber'},
333                                                                                    message_name   => 'advance_notice' } );
334         next UPCOMINGITEM unless $borrower_preferences && exists $borrower_preferences->{'days_in_advance'};
335         next UPCOMINGITEM unless $borrower_preferences->{'days_in_advance'} == $upcoming->{'days_until_due'};
336
337         if ( $borrower_preferences->{'wants_digest'} ) {
338             # cache this one to process after we've run through all of the items.
339             if ($digest_per_branch) {
340                 $upcoming_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{email} = $from_address;
341                 $upcoming_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{count}++;
342             } else {
343                 $upcoming_digest->{ $upcoming->{borrowernumber} }->{email} = $from_address;
344                 $upcoming_digest->{ $upcoming->{borrowernumber} }->{count}++;
345             }
346         } else {
347         my $branchcode;
348         if($owning_library) {
349         $branchcode = $upcoming->{'homebranch'};
350         } else {
351         $branchcode = $upcoming->{'branchcode'};
352         }
353         # Skip this PREDUE if we specify list of libraries and this one is not part of it
354         next if (@branchcodes && !$branches{$branchcode});
355
356             my $item = Koha::Items->find( $upcoming->{itemnumber} );
357             my $letter_type = 'PREDUE';
358             $sth->execute($upcoming->{'borrowernumber'},$upcoming->{'itemnumber'},$borrower_preferences->{'days_in_advance'});
359             my $titles = "";
360             while ( my $item_info = $sth->fetchrow_hashref()) {
361                 $titles .= C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields } );
362             }
363
364             ## Get branch info for borrowers home library.
365             foreach my $transport ( keys %{$borrower_preferences->{'transports'}} ) {
366                 my $letter = parse_letter( { letter_code    => $letter_type,
367                                       borrowernumber => $upcoming->{'borrowernumber'},
368                                       branchcode     => $branchcode,
369                                       biblionumber   => $item->biblionumber,
370                                       itemnumber     => $upcoming->{'itemnumber'},
371                                       substitute     => { 'items.content' => $titles },
372                                       message_transport_type => $transport,
373                                     } )
374                     or warn "no letter of type '$letter_type' found for borrowernumber ".$upcoming->{'borrowernumber'}.". Please see sample_notices.sql";
375                 push @letters, $letter if $letter;
376             }
377         }
378     }
379
380     # If we have prepared a letter, send it.
381     if ( @letters ) {
382       if ($nomail) {
383         for my $letter ( @letters ) {
384             local $, = "\f";
385             print $letter->{'content'};
386         }
387       }
388       else {
389         for my $letter ( @letters ) {
390             C4::Letters::EnqueueLetter( { letter                 => $letter,
391                                           borrowernumber         => $upcoming->{'borrowernumber'},
392                                           from_address           => $from_address,
393                                           message_transport_type => $letter->{message_transport_type} } );
394         }
395       }
396     }
397 }
398
399
400
401 # Now, run through all the people that want digests and send them
402
403 my $sth_digest = $dbh->prepare(<<'END_SQL');
404 SELECT biblio.*, items.*, issues.*
405   FROM issues,items,biblio
406   WHERE items.itemnumber=issues.itemnumber
407     AND biblio.biblionumber=items.biblionumber
408     AND issues.borrowernumber = ?
409     AND (TO_DAYS(date_due)-TO_DAYS(NOW()) = ?)
410 END_SQL
411
412 if ($digest_per_branch) {
413     while (my ($branchcode, $digests) = each %$upcoming_digest) {
414         send_digests({
415             sth => $sth_digest,
416             digests => $digests,
417             letter_code => 'PREDUEDGST',
418             message_name => 'advance_notice',
419             branchcode => $branchcode,
420             get_item_info => sub {
421                 my $params = shift;
422                 $params->{sth}->execute($params->{borrowernumber},
423                                         $params->{borrower_preferences}->{'days_in_advance'});
424                 return sub {
425                     $params->{sth}->fetchrow_hashref;
426                 };
427             }
428         });
429     }
430
431     while (my ($branchcode, $digests) = each %$due_digest) {
432         send_digests({
433             sth => $sth_digest,
434             digests => $due_digest,
435             letter_code => 'DUEDGST',
436             branchcode => $branchcode,
437             message_name => 'item_due',
438             get_item_info => sub {
439                 my $params = shift;
440                 $params->{sth}->execute($params->{borrowernumber}, 0);
441                 return sub {
442                     $params->{sth}->fetchrow_hashref;
443                 };
444             }
445         });
446     }
447 } else {
448     send_digests({
449         sth => $sth_digest,
450         digests => $upcoming_digest,
451         letter_code => 'PREDUEDGST',
452         message_name => 'advance_notice',
453         get_item_info => sub {
454             my $params = shift;
455             $params->{sth}->execute($params->{borrowernumber},
456                                     $params->{borrower_preferences}->{'days_in_advance'});
457             return sub {
458                 $params->{sth}->fetchrow_hashref;
459             };
460         }
461     });
462
463     send_digests({
464         sth => $sth_digest,
465         digests => $due_digest,
466         letter_code => 'DUEDGST',
467         message_name => 'item_due',
468         get_item_info => sub {
469             my $params = shift;
470             $params->{sth}->execute($params->{borrowernumber}, 0);
471             return sub {
472                 $params->{sth}->fetchrow_hashref;
473             };
474         }
475     });
476 }
477
478 =head1 METHODS
479
480 =head2 parse_letter
481
482 =cut
483
484 sub parse_letter {
485     my $params = shift;
486
487     foreach my $required ( qw( letter_code borrowernumber ) ) {
488         return unless exists $params->{$required};
489     }
490     my $patron = Koha::Patrons->find( $params->{borrowernumber} );
491
492     my %table_params = ( 'borrowers' => $params->{'borrowernumber'} );
493
494     if ( my $p = $params->{'branchcode'} ) {
495         $table_params{'branches'} = $p;
496     }
497     if ( my $p = $params->{'itemnumber'} ) {
498         $table_params{'issues'} = $p;
499         $table_params{'items'} = $p;
500     }
501     if ( my $p = $params->{'biblionumber'} ) {
502         $table_params{'biblio'} = $p;
503         $table_params{'biblioitems'} = $p;
504     }
505
506     return C4::Letters::GetPreparedLetter (
507         module => 'circulation',
508         letter_code => $params->{'letter_code'},
509         branchcode => $table_params{'branches'},
510         lang => $patron->lang,
511         substitute => $params->{'substitute'},
512         tables     => \%table_params,
513         message_transport_type => $params->{message_transport_type},
514     );
515 }
516
517 =head2 get_branch_info
518
519 =cut
520
521 sub get_branch_info {
522     my ( $borrowernumber ) = @_;
523
524     ## Get branch info for borrowers home library.
525     my $patron = Koha::Patrons->find( $borrowernumber );
526     my $branch = $patron->library->unblessed;
527     my %branch_info;
528     foreach my $key( keys %$branch ) {
529         $branch_info{"branches.$key"} = $branch->{$key};
530     }
531
532     return %branch_info;
533 }
534
535 =head2 send_digests
536
537     send_digests({
538         digests => ...,
539         sth => ...,
540         letter_code => ...,
541         get_item_info => ...,
542     })
543
544 Enqueue digested letters (or print them if -n was passed at command line).
545
546 Parameters:
547
548 =over 4
549
550 =item C<$digests>
551
552 Reference to the array of digested messages.
553
554 =item C<$sth>
555
556 Prepared statement handle for fetching overdue issues.
557
558 =item C<$letter_code>
559
560 String that denote the letter code.
561
562 =item C<$get_item_info>
563
564 Subroutine for executing prepared statement.  Takes parameters $sth,
565 $borrowernumber and $borrower_parameters and return a generator
566 function that produce the matching rows.
567
568 =back
569
570 =cut
571
572 sub send_digests {
573     my $params = shift;
574
575     PATRON: while ( my ( $borrowernumber, $digest ) = each %{$params->{digests}} ) {
576         @letters = ();
577         my $count = $digest->{count};
578         my $from_address = $digest->{email};
579
580         my %branch_info;
581         my $branchcode;
582
583         if (defined($params->{branchcode})) {
584             %branch_info = ();
585             $branchcode = $params->{branchcode};
586         } else {
587             ## Get branch info for borrowers home library.
588             %branch_info = get_branch_info( $borrowernumber );
589             $branchcode = $branch_info{'branches.branchcode'};
590         }
591
592         my $borrower_preferences =
593             C4::Members::Messaging::GetMessagingPreferences(
594                 {
595                     borrowernumber => $borrowernumber,
596                     message_name   => $params->{message_name}
597                 }
598             );
599
600         next PATRON unless $borrower_preferences; # how could this happen?
601
602         my $next_item_info = $params->{get_item_info}->({
603             sth => $params->{sth},
604             borrowernumber => $borrowernumber,
605             borrower_preferences => $borrower_preferences
606         });
607         my $titles = "";
608         while ( my $item_info = $next_item_info->()) {
609             $titles .= C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields } );
610         }
611
612         foreach my $transport ( keys %{ $borrower_preferences->{'transports'} } ) {
613             my $letter = parse_letter(
614                 {
615                     letter_code    => $params->{letter_code},
616                     borrowernumber => $borrowernumber,
617                     substitute     => {
618                         count           => $count,
619                         'items.content' => $titles,
620                         %branch_info
621                     },
622                     branchcode     => $branchcode,
623                     message_transport_type => $transport
624                 }
625             );
626             unless ( $letter ){
627                 warn "no letter of type '$params->{letter_type}' found for borrowernumber $borrowernumber. Please see sample_notices.sql";
628                 next;
629             }
630             push @letters, $letter if $letter;
631         }
632
633         if ( @letters ) {
634             if ($nomail) {
635                 for my $letter ( @letters ) {
636                     local $, = "\f";
637                     print $letter->{'content'};
638                 }
639             }
640             else {
641                 for my $letter ( @letters ) {
642                     C4::Letters::EnqueueLetter( { letter                 => $letter,
643                                                   borrowernumber         => $borrowernumber,
644                                                   from_address           => $from_address,
645                                                   message_transport_type => $letter->{message_transport_type} } );
646                 }
647             }
648         }
649     }
650 }
651
652
653 1;
654
655 __END__