Bug 18276: Remove GetBiblioFromItemNumber - Easy ones
[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 C4::Biblio;
53 use C4::Context;
54 use C4::Letters;
55 use C4::Members;
56 use C4::Members::Messaging;
57 use C4::Overdues;
58 use Koha::DateUtils;
59 use C4::Log;
60 use Koha::Items;
61 use Koha::Libraries;
62 use Koha::Patrons;
63
64 =head1 NAME
65
66 advance_notices.pl - prepare messages to be sent to patrons for nearly due, or due, items
67
68 =head1 SYNOPSIS
69
70 advance_notices.pl
71   [ -n ][ -m <number of days> ][ --itemscontent <comma separated field list> ][ -c ]
72
73 =head1 OPTIONS
74
75 =over 8
76
77 =item B<--help>
78
79 Print a brief help message and exits.
80
81 =item B<--man>
82
83 Prints the manual page and exits.
84
85 =item B<-v>
86
87 Verbose. Without this flag set, only fatal errors are reported.
88
89 =item B<-n>
90
91 Do not send any email. Advanced or due notices that would have been sent to
92 the patrons are printed to standard out.
93
94 =item B<-m>
95
96 Defines the maximum number of days in advance to send advance notices.
97
98 =item B<-c>
99
100 Confirm flag: Add this option. The script will only print a usage
101 statement otherwise.
102
103 =item B<--itemscontent>
104
105 comma separated list of fields that get substituted into templates in
106 places of the E<lt>E<lt>items.contentE<gt>E<gt> placeholder. This
107 defaults to date_due,title,author,barcode
108
109 Other possible values come from fields in the biblios, items and
110 issues tables.
111
112 =back
113
114 =head1 DESCRIPTION
115
116 This script is designed to alert patrons when items are due, or coming due
117
118 =head2 Configuration
119
120 This script pays attention to the advanced notice configuration
121 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
122 section of Koha is available in the Koha manual.
123
124 =head2 Outgoing emails
125
126 Typically, messages are prepared for each patron with due
127 items, and who have selected (or the library has elected for them) Advance or Due notices.
128
129 These emails are staged in the outgoing message queue, as are messages
130 produced by other features of Koha. This message queue must be
131 processed regularly by the
132 F<misc/cronjobs/process_message_queue.pl> program.
133
134 In the event that the C<-n> flag is passed to this program, no emails
135 are sent. Instead, messages are sent on standard output from this
136 program. They may be redirected to a file if desired.
137
138 =head2 Templates
139
140 Templates can contain variables enclosed in double angle brackets like
141 E<lt>E<lt>thisE<gt>E<gt>. Those variables will be replaced with values
142 specific to the overdue items or relevant patron. Available variables
143 are:
144
145 =over
146
147 =item E<lt>E<lt>items.contentE<gt>E<gt>
148
149 one line for each item, each line containing a tab separated list of
150 date due, title, author, barcode
151
152 =item E<lt>E<lt>borrowers.*E<gt>E<gt>
153
154 any field from the borrowers table
155
156 =item E<lt>E<lt>branches.*E<gt>E<gt>
157
158 any field from the branches table
159
160 =back
161
162 =head1 SEE ALSO
163
164 The F<misc/cronjobs/overdue_notices.pl> program allows you to send
165 messages to patrons when their messages are overdue.
166 =cut
167
168 binmode( STDOUT, ':encoding(UTF-8)' );
169
170 # These are defaults for command line options.
171 my $confirm;                                                        # -c: Confirm that the user has read and configured this script.
172 my $nomail;                                                         # -n: No mail. Will not send any emails.
173 my $mindays     = 0;                                                # -m: Maximum number of days in advance to send notices
174 my $maxdays     = 30;                                               # -e: the End of the time period
175 my $verbose     = 0;                                                # -v: verbose
176 my $itemscontent = join(',',qw( date_due title author barcode ));
177
178 my $help    = 0;
179 my $man     = 0;
180
181 GetOptions(
182             'help|?'         => \$help,
183             'man'            => \$man,
184             'c'              => \$confirm,
185             'n'              => \$nomail,
186             'm:i'            => \$maxdays,
187             'v'              => \$verbose,
188             'itemscontent=s' => \$itemscontent,
189        )or pod2usage(2);
190 pod2usage(1) if $help;
191 pod2usage( -verbose => 2 ) if $man;
192
193 # Since advance notice options are not visible in the web-interface
194 # unless EnhancedMessagingPreferences is on, let the user know that
195 # this script probably isn't going to do much
196 if ( ! C4::Context->preference('EnhancedMessagingPreferences') ) {
197     warn <<'END_WARN';
198
199 The "EnhancedMessagingPreferences" syspref is off.
200 Therefore, it is unlikely that this script will actually produce any messages to be sent.
201 To change this, edit the "EnhancedMessagingPreferences" syspref.
202
203 END_WARN
204 }
205 unless ($confirm) {
206      pod2usage(1);
207 }
208
209 cronlogaction();
210
211 # The fields that will be substituted into <<items.content>>
212 my @item_content_fields = split(/,/,$itemscontent);
213
214 warn 'getting upcoming due issues' if $verbose;
215 my $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $maxdays } );
216 warn 'found ' . scalar( @$upcoming_dues ) . ' issues' if $verbose;
217
218 # hash of borrowernumber to number of items upcoming
219 # for patrons wishing digests only.
220 my $upcoming_digest;
221 my $due_digest;
222
223 my $dbh = C4::Context->dbh();
224 my $sth = $dbh->prepare(<<'END_SQL');
225 SELECT biblio.*, items.*, issues.*
226   FROM issues,items,biblio
227   WHERE items.itemnumber=issues.itemnumber
228     AND biblio.biblionumber=items.biblionumber
229     AND issues.borrowernumber = ?
230     AND issues.itemnumber = ?
231     AND (TO_DAYS(date_due)-TO_DAYS(NOW()) = ?)
232 END_SQL
233
234 my $admin_adress = C4::Context->preference('KohaAdminEmailAddress');
235
236 my @letters;
237 UPCOMINGITEM: foreach my $upcoming ( @$upcoming_dues ) {
238     @letters = ();
239     warn 'examining ' . $upcoming->{'itemnumber'} . ' upcoming due items' if $verbose;
240
241     my $from_address = $upcoming->{branchemail} || $admin_adress;
242
243     my $borrower_preferences;
244     if ( 0 == $upcoming->{'days_until_due'} ) {
245         # This item is due today. Send an 'item due' message.
246         $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $upcoming->{'borrowernumber'},
247                                                                                    message_name   => 'item_due' } );
248         next unless $borrower_preferences;
249         
250         if ( $borrower_preferences->{'wants_digest'} ) {
251             # cache this one to process after we've run through all of the items.
252             $due_digest->{ $upcoming->{borrowernumber} }->{email} = $from_address;
253             $due_digest->{ $upcoming->{borrowernumber} }->{count}++;
254         } else {
255             my $item = Koha::Items->find( $upcoming->{itemnumber} );
256             my $letter_type = 'DUE';
257             $sth->execute($upcoming->{'borrowernumber'},$upcoming->{'itemnumber'},'0');
258             my $titles = "";
259             while ( my $item_info = $sth->fetchrow_hashref()) {
260               my @item_info = map { $_ =~ /^date|date$/ ? format_date($item_info->{$_}) : $item_info->{$_} || '' } @item_content_fields;
261               $titles .= join("\t",@item_info) . "\n";
262             }
263
264             ## Get branch info for borrowers home library.
265             foreach my $transport ( keys %{$borrower_preferences->{'transports'}} ) {
266                 my $letter = parse_letter( { letter_code    => $letter_type,
267                                       borrowernumber => $upcoming->{'borrowernumber'},
268                                       branchcode     => $upcoming->{'branchcode'},
269                                       biblionumber   => $item->biblionumber,
270                                       itemnumber     => $upcoming->{'itemnumber'},
271                                       substitute     => { 'items.content' => $titles },
272                                       message_transport_type => $transport,
273                                     } )
274                     or warn "no letter of type '$letter_type' found for borrowernumber ".$upcoming->{'borrowernumber'}.". Please see sample_notices.sql";
275                 push @letters, $letter if $letter;
276             }
277         }
278     } else {
279         $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $upcoming->{'borrowernumber'},
280                                                                                    message_name   => 'advance_notice' } );
281         next UPCOMINGITEM unless $borrower_preferences && exists $borrower_preferences->{'days_in_advance'};
282         next UPCOMINGITEM unless $borrower_preferences->{'days_in_advance'} == $upcoming->{'days_until_due'};
283
284         if ( $borrower_preferences->{'wants_digest'} ) {
285             # cache this one to process after we've run through all of the items.
286             $upcoming_digest->{ $upcoming->{borrowernumber} }->{email} = $from_address;
287             $upcoming_digest->{ $upcoming->{borrowernumber} }->{count}++;
288         } else {
289             my $item = Koha::Items->find( $upcoming->{itemnumber} );
290             my $letter_type = 'PREDUE';
291             $sth->execute($upcoming->{'borrowernumber'},$upcoming->{'itemnumber'},$borrower_preferences->{'days_in_advance'});
292             my $titles = "";
293             while ( my $item_info = $sth->fetchrow_hashref()) {
294               my @item_info = map { $_ =~ /^date|date$/ ? format_date($item_info->{$_}) : $item_info->{$_} || '' } @item_content_fields;
295               $titles .= join("\t",@item_info) . "\n";
296             }
297
298             ## Get branch info for borrowers home library.
299             foreach my $transport ( keys %{$borrower_preferences->{'transports'}} ) {
300                 my $letter = parse_letter( { letter_code    => $letter_type,
301                                       borrowernumber => $upcoming->{'borrowernumber'},
302                                       branchcode     => $upcoming->{'branchcode'},
303                                       biblionumber   => $item->biblionumber,
304                                       itemnumber     => $upcoming->{'itemnumber'},
305                                       substitute     => { 'items.content' => $titles },
306                                       message_transport_type => $transport,
307                                     } )
308                     or warn "no letter of type '$letter_type' found for borrowernumber ".$upcoming->{'borrowernumber'}.". Please see sample_notices.sql";
309                 push @letters, $letter if $letter;
310             }
311         }
312     }
313
314     # If we have prepared a letter, send it.
315     if ( @letters ) {
316       if ($nomail) {
317         for my $letter ( @letters ) {
318             local $, = "\f";
319             print $letter->{'content'};
320         }
321       }
322       else {
323         for my $letter ( @letters ) {
324             C4::Letters::EnqueueLetter( { letter                 => $letter,
325                                           borrowernumber         => $upcoming->{'borrowernumber'},
326                                           from_address           => $from_address,
327                                           message_transport_type => $letter->{message_transport_type} } );
328         }
329       }
330     }
331 }
332
333
334
335 # Now, run through all the people that want digests and send them
336
337 $sth = $dbh->prepare(<<'END_SQL');
338 SELECT biblio.*, items.*, issues.*
339   FROM issues,items,biblio
340   WHERE items.itemnumber=issues.itemnumber
341     AND biblio.biblionumber=items.biblionumber
342     AND issues.borrowernumber = ?
343     AND (TO_DAYS(date_due)-TO_DAYS(NOW()) = ?)
344 END_SQL
345 PATRON: while ( my ( $borrowernumber, $digest ) = each %$upcoming_digest ) {
346     @letters = ();
347     my $count = $digest->{count};
348     my $from_address = $digest->{email};
349
350     my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber,
351                                                                                   message_name   => 'advance_notice' } );
352     next PATRON unless $borrower_preferences; # how could this happen?
353
354
355     my $letter_type = 'PREDUEDGST';
356
357     $sth->execute($borrowernumber,$borrower_preferences->{'days_in_advance'});
358     my $titles = "";
359     while ( my $item_info = $sth->fetchrow_hashref()) {
360       my @item_info = map { $_ =~ /^date|date$/ ? format_date($item_info->{$_}) : $item_info->{$_} || '' } @item_content_fields;
361       $titles .= join("\t",@item_info) . "\n";
362     }
363
364     ## Get branch info for borrowers home library.
365     my %branch_info = get_branch_info( $borrowernumber );
366
367     foreach my $transport ( keys %{ $borrower_preferences->{'transports'} } ) {
368         my $letter = parse_letter(
369             {
370                 letter_code    => $letter_type,
371                 borrowernumber => $borrowernumber,
372                 substitute     => {
373                     count           => $count,
374                     'items.content' => $titles,
375                     %branch_info,
376                 },
377                 branchcode             => $branch_info{"branches.branchcode"},
378                 message_transport_type => $transport,
379             }
380           )
381           or warn "no letter of type '$letter_type' found for borrowernumber $borrowernumber. Please see sample_notices.sql";
382         push @letters, $letter if $letter;
383     }
384
385     if ( @letters ) {
386       if ($nomail) {
387         for my $letter ( @letters ) {
388             local $, = "\f";
389             print $letter->{'content'};
390         }
391       }
392       else {
393         for my $letter ( @letters ) {
394             C4::Letters::EnqueueLetter( { letter                 => $letter,
395                                           borrowernumber         => $borrowernumber,
396                                           from_address           => $from_address,
397                                           message_transport_type => $letter->{message_transport_type} } );
398         }
399       }
400     }
401 }
402
403 # Now, run through all the people that want digests and send them
404 PATRON: while ( my ( $borrowernumber, $digest ) = each %$due_digest ) {
405     @letters = ();
406     my $count = $digest->{count};
407     my $from_address = $digest->{email};
408
409     my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber,
410                                                                                   message_name   => 'item_due' } );
411     next PATRON unless $borrower_preferences; # how could this happen?
412
413     my $letter_type = 'DUEDGST';
414     $sth->execute($borrowernumber,'0');
415     my $titles = "";
416     while ( my $item_info = $sth->fetchrow_hashref()) {
417       my @item_info = map { $_ =~ /^date|date$/ ? format_date($item_info->{$_}) : $item_info->{$_} || '' } @item_content_fields;
418       $titles .= join("\t",@item_info) . "\n";
419     }
420
421     ## Get branch info for borrowers home library.
422     my %branch_info = get_branch_info( $borrowernumber );
423
424     for my $transport ( keys %{ $borrower_preferences->{'transports'} } ) {
425         my $letter = parse_letter(
426             {
427                 letter_code    => $letter_type,
428                 borrowernumber => $borrowernumber,
429                 substitute     => {
430                     count           => $count,
431                     'items.content' => $titles,
432                     %branch_info,
433                 },
434                 branchcode             => $branch_info{"branches.branchcode"},
435                 message_transport_type => $transport,
436             }
437           )
438           or warn "no letter of type '$letter_type' found for borrowernumber $borrowernumber. Please see sample_notices.sql";
439         push @letters, $letter if $letter;
440     }
441
442     if ( @letters ) {
443       if ($nomail) {
444         for my $letter ( @letters ) {
445             local $, = "\f";
446             print $letter->{'content'};
447         }
448       }
449       else {
450         for my $letter ( @letters ) {
451             C4::Letters::EnqueueLetter( { letter                 => $letter,
452                                           borrowernumber         => $borrowernumber,
453                                           from_address           => $from_address,
454                                           message_transport_type => $letter->{message_transport_type} } );
455         }
456       }
457     }
458
459 }
460
461 =head1 METHODS
462
463 =head2 parse_letter
464
465 =cut
466
467 sub parse_letter {
468     my $params = shift;
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         message_transport_type => $params->{message_transport_type},
496     );
497 }
498
499 sub format_date {
500     my $date_string = shift;
501     my $dt=dt_from_string($date_string);
502     return output_pref($dt);
503 }
504
505 =head2 get_branch_info
506
507 =cut
508
509 sub get_branch_info {
510     my ( $borrowernumber ) = @_;
511
512     ## Get branch info for borrowers home library.
513     my $borrower_details = C4::Members::GetMember( borrowernumber => $borrowernumber );
514     my $borrower_branchcode = $borrower_details->{'branchcode'};
515     my $branch = Koha::Libraries->find( $borrower_branchcode )->unblessed;
516     my %branch_info;
517     foreach my $key( keys %$branch ) {
518         $branch_info{"branches.$key"} = $branch->{$key};
519     }
520
521     return %branch_info;
522 }
523
524 1;
525
526 __END__