Merge remote-tracking branch 'kc/new/enh/bug_4877' into kcmaster
[wip/koha-chris_n.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 under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 2 of the License, or (at your option) any later
10 # version.
11 #
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
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 Data::Dumper;
45 BEGIN {
46     # find Koha's Perl modules
47     # test carefully before changing this
48     use FindBin;
49     eval { require "$FindBin::Bin/../kohalib.pl" };
50 }
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 C4::Dates qw/format_date/;
58
59
60 # These are defaults for command line options.
61 my $confirm;                                                        # -c: Confirm that the user has read and configured this script.
62 # my $confirm     = 1;                                                # -c: Confirm that the user has read and configured this script.
63 my $nomail;                                                         # -n: No mail. Will not send any emails.
64 my $mindays     = 0;                                                # -m: Maximum number of days in advance to send notices
65 my $maxdays     = 30;                                               # -e: the End of the time period
66 my $verbose     = 0;                                                # -v: verbose
67 my $itemscontent = join(',',qw( issuedate title barcode author ));
68
69 GetOptions( 'c'              => \$confirm,
70             'n'              => \$nomail,
71             'm:i'            => \$maxdays,
72             'v'              => \$verbose,
73             'itemscontent=s' => \$itemscontent,
74        );
75 my $usage = << 'ENDUSAGE';
76
77 This script prepares pre-due and item due reminders to be sent to
78 patrons. It queues them in the message queue, which is processed by
79 the process_message_queue.pl cronjob.
80 See the comments in the script for directions on changing the script.
81 This script has the following parameters :
82         -c Confirm and remove this help & warning
83         -m maximum number of days in advance to send advance notices.
84         -n send No mail. Instead, all mail messages are printed on screen. Usefull for testing purposes.
85         -v verbose
86         -i csv list of fields that get substituted into templates in places
87            of the E<lt>E<lt>items.contentE<gt>E<gt> placeholder.  Defaults to
88            issuedate,title,barcode,author
89 ENDUSAGE
90
91 # Since advance notice options are not visible in the web-interface
92 # unless EnhancedMessagingPreferences is on, let the user know that
93 # this script probably isn't going to do much
94 if ( ! C4::Context->preference('EnhancedMessagingPreferences') ) {
95     warn <<'END_WARN';
96
97 The "EnhancedMessagingPreferences" syspref is off.
98 Therefore, it is unlikely that this script will actually produce any messages to be sent.
99 To change this, edit the "EnhancedMessagingPreferences" syspref.
100
101 END_WARN
102 }
103
104 unless ($confirm) {
105     print $usage;
106     print "Do you wish to continue? (y/n)";
107     chomp($_ = <STDIN>);
108     exit unless (/^y/i);
109         
110 }
111
112 # The fields that will be substituted into <<items.content>>
113 my @item_content_fields = split(/,/,$itemscontent);
114
115 warn 'getting upcoming due issues' if $verbose;
116 my $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $maxdays } );
117 warn 'found ' . scalar( @$upcoming_dues ) . ' issues' if $verbose;
118
119 # hash of borrowernumber to number of items upcoming
120 # for patrons wishing digests only.
121 my $upcoming_digest;
122 my $due_digest;
123
124 my $dbh = C4::Context->dbh();
125 my $sth = $dbh->prepare(<<'END_SQL');
126 SELECT biblio.*, items.*, issues.*
127   FROM issues,items,biblio
128   WHERE items.itemnumber=issues.itemnumber
129     AND biblio.biblionumber=items.biblionumber
130     AND issues.borrowernumber = ?
131     AND issues.itemnumber = ?
132     AND (TO_DAYS(date_due)-TO_DAYS(NOW()) = ?)
133 END_SQL
134
135 my $admin_adress = C4::Context->preference('KohaAdminEmailAddress');
136
137 UPCOMINGITEM: foreach my $upcoming ( @$upcoming_dues ) {
138     warn 'examining ' . $upcoming->{'itemnumber'} . ' upcoming due items' if $verbose;
139     # warn( Data::Dumper->Dump( [ $upcoming ], [ 'overdue' ] ) );
140
141     my $from_address = $upcoming->{branchemail} || $admin_adress;
142
143     my $letter;
144     my $borrower_preferences;
145     if ( 0 == $upcoming->{'days_until_due'} ) {
146         # This item is due today. Send an 'item due' message.
147         $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $upcoming->{'borrowernumber'},
148                                                                                    message_name   => 'item_due' } );
149         # warn( Data::Dumper->Dump( [ $borrower_preferences ], [ 'borrower_preferences' ] ) );
150         next unless $borrower_preferences;
151         
152         if ( $borrower_preferences->{'wants_digest'} ) {
153             # cache this one to process after we've run through all of the items.
154             my $digest = $due_digest->{$upcoming->{'borrowernumber'}} ||= {};
155             $digest->{email} ||= $from_address;
156             $digest->{count}++;
157         } else {
158             my $biblio = C4::Biblio::GetBiblioFromItemNumber( $upcoming->{'itemnumber'} );
159             my $letter_type = 'DUE';
160             $letter = C4::Letters::getletter( 'circulation', $letter_type );
161             die "no letter of type '$letter_type' found. Please see sample_notices.sql" unless $letter;
162             $sth->execute($upcoming->{'borrowernumber'},$upcoming->{'itemnumber'},'0');
163             my $titles = "";
164             while ( my $item_info = $sth->fetchrow_hashref()) {
165               my @item_info = map { $_ =~ /^date|date$/ ? format_date($item_info->{$_}) : $item_info->{$_} || '' } @item_content_fields;
166               $titles .= join("\t",@item_info) . "\n";
167             }
168         
169             $letter = parse_letter( { letter         => $letter,
170                                       borrowernumber => $upcoming->{'borrowernumber'},
171                                       branchcode     => $upcoming->{'branchcode'},
172                                       biblionumber   => $biblio->{'biblionumber'},
173                                       itemnumber     => $upcoming->{'itemnumber'},
174                                       substitute     => { 'items.content' => $titles }
175                                     } );
176         }
177     } else {
178         $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $upcoming->{'borrowernumber'},
179                                                                                    message_name   => 'advance_notice' } );
180         # warn( Data::Dumper->Dump( [ $borrower_preferences ], [ 'borrower_preferences' ] ) );
181         next UPCOMINGITEM unless $borrower_preferences && exists $borrower_preferences->{'days_in_advance'};
182         next UPCOMINGITEM unless $borrower_preferences->{'days_in_advance'} == $upcoming->{'days_until_due'};
183
184         if ( $borrower_preferences->{'wants_digest'} ) {
185             # cache this one to process after we've run through all of the items.
186             my $digest = $upcoming_digest->{$upcoming->{'borrowernumber'}} ||= {};
187             $digest->{email} ||= $from_address;
188             $digest->{count}++;
189         } else {
190             my $biblio = C4::Biblio::GetBiblioFromItemNumber( $upcoming->{'itemnumber'} );
191             my $letter_type = 'PREDUE';
192             $letter = C4::Letters::getletter( 'circulation', $letter_type );
193             die "no letter of type '$letter_type' found. Please see sample_notices.sql" unless $letter;
194             $sth->execute($upcoming->{'borrowernumber'},$upcoming->{'itemnumber'},$borrower_preferences->{'days_in_advance'});
195             my $titles = "";
196             while ( my $item_info = $sth->fetchrow_hashref()) {
197               my @item_info = map { $_ =~ /^date|date$/ ? format_date($item_info->{$_}) : $item_info->{$_} || '' } @item_content_fields;
198               $titles .= join("\t",@item_info) . "\n";
199             }
200         
201             $letter = parse_letter( { letter         => $letter,
202                                       borrowernumber => $upcoming->{'borrowernumber'},
203                                       branchcode     => $upcoming->{'branchcode'},
204                                       biblionumber   => $biblio->{'biblionumber'},
205                                       itemnumber     => $upcoming->{'itemnumber'},
206                                       substitute     => { 'items.content' => $titles }
207                                     } );
208         }
209     }
210
211     # If we have prepared a letter, send it.
212     if ($letter) {
213       if ($nomail) {
214         local $, = "\f";
215         print $letter->{'content'};
216       }
217       else {
218         foreach my $transport ( @{$borrower_preferences->{'transports'}} ) {
219             C4::Letters::EnqueueLetter( { letter                 => $letter,
220                                           borrowernumber         => $upcoming->{'borrowernumber'},
221                                           from_address           => $from_address,
222                                           message_transport_type => $transport } );
223         }
224       }
225     }
226 }
227
228
229 # warn( Data::Dumper->Dump( [ $upcoming_digest ], [ 'upcoming_digest' ] ) );
230
231 # Now, run through all the people that want digests and send them
232
233 $sth = $dbh->prepare(<<'END_SQL');
234 SELECT biblio.*, items.*, issues.*
235   FROM issues,items,biblio
236   WHERE items.itemnumber=issues.itemnumber
237     AND biblio.biblionumber=items.biblionumber
238     AND issues.borrowernumber = ?
239     AND (TO_DAYS(date_due)-TO_DAYS(NOW()) = ?)
240 END_SQL
241
242 PATRON: while ( my ( $borrowernumber, $digest ) = each %$upcoming_digest ) {
243     my $count = $digest->{count};
244     my $from_address = $digest->{email};
245
246     my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber,
247                                                                                   message_name   => 'advance_notice' } );
248     # warn( Data::Dumper->Dump( [ $borrower_preferences ], [ 'borrower_preferences' ] ) );
249     next PATRON unless $borrower_preferences; # how could this happen?
250
251
252     my $letter_type = 'PREDUEDGST';
253     my $letter = C4::Letters::getletter( 'circulation', $letter_type );
254     die "no letter of type '$letter_type' found. Please see sample_notices.sql" unless $letter;
255
256     $sth->execute($borrowernumber,$borrower_preferences->{'days_in_advance'});
257     my $titles = "";
258     while ( my $item_info = $sth->fetchrow_hashref()) {
259       my @item_info = map { $_ =~ /^date|date$/ ? format_date($item_info->{$_}) : $item_info->{$_} || '' } @item_content_fields;
260       $titles .= join("\t",@item_info) . "\n";
261     }
262     $letter = parse_letter( { letter         => $letter,
263                               borrowernumber => $borrowernumber,
264                               substitute     => { count => $count,
265                                                   'items.content' => $titles
266                                                 }
267                          } );
268     if ($nomail) {
269       local $, = "\f";
270       print $letter->{'content'};
271     }
272     else {
273       foreach my $transport ( @{$borrower_preferences->{'transports'}} ) {
274         C4::Letters::EnqueueLetter( { letter                 => $letter,
275                                       borrowernumber         => $borrowernumber,
276                                       from_address           => $from_address,
277                                       message_transport_type => $transport } );
278       }
279     }
280 }
281
282 # Now, run through all the people that want digests and send them
283 PATRON: while ( my ( $borrowernumber, $digest ) = each %$due_digest ) {
284     my $count = $digest->{count};
285     my $from_address = $digest->{email};
286
287     my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber,
288                                                                                   message_name   => 'item_due' } );
289     # warn( Data::Dumper->Dump( [ $borrower_preferences ], [ 'borrower_preferences' ] ) );
290     next PATRON unless $borrower_preferences; # how could this happen?
291
292     my $letter_type = 'DUEDGST';
293     my $letter = C4::Letters::getletter( 'circulation', $letter_type );
294     die "no letter of type '$letter_type' found. Please see sample_notices.sql" unless $letter;
295     $sth->execute($borrowernumber,'0');
296     my $titles = "";
297     while ( my $item_info = $sth->fetchrow_hashref()) {
298       my @item_info = map { $_ =~ /^date|date$/ ? format_date($item_info->{$_}) : $item_info->{$_} || '' } @item_content_fields;
299       $titles .= join("\t",@item_info) . "\n";
300     }
301     $letter = parse_letter( { letter         => $letter,
302                               borrowernumber => $borrowernumber,
303                               substitute     => { count => $count,
304                                                   'items.content' => $titles
305                                                 }
306                          } );
307
308     if ($nomail) {
309       local $, = "\f";
310       print $letter->{'content'};
311     }
312     else {
313       foreach my $transport ( @{$borrower_preferences->{'transports'}} ) {
314         C4::Letters::EnqueueLetter( { letter                 => $letter,
315                                       borrowernumber         => $borrowernumber,
316                                       from_address           => $from_address,
317                                       message_transport_type => $transport } );
318       }
319     }
320 }
321
322 =head1 METHODS
323
324 =head2 parse_letter
325
326
327
328 =cut
329
330 sub parse_letter {
331     my $params = shift;
332     foreach my $required ( qw( letter borrowernumber ) ) {
333         return unless exists $params->{$required};
334     }
335
336     if ( $params->{'substitute'} ) {
337         while ( my ($key, $replacedby) = each %{$params->{'substitute'}} ) {
338             my $replacefield = "<<$key>>";
339             
340             $params->{'letter'}->{title}   =~ s/$replacefield/$replacedby/g;
341             $params->{'letter'}->{content} =~ s/$replacefield/$replacedby/g;
342         }
343     }
344
345     C4::Letters::parseletter( $params->{'letter'}, 'borrowers',   $params->{'borrowernumber'} );
346
347     if ( $params->{'branchcode'} ) {
348         C4::Letters::parseletter( $params->{'letter'}, 'branches',    $params->{'branchcode'} );
349     }
350     if ( $params->{'itemnumber'} ) {
351         C4::Letters::parseletter( $params->{'letter'}, 'issues', $params->{'itemnumber'} );
352     }
353     if ( $params->{'biblionumber'} ) {
354         C4::Letters::parseletter( $params->{'letter'}, 'biblio',      $params->{'biblionumber'} );
355         C4::Letters::parseletter( $params->{'letter'}, 'biblioitems', $params->{'biblionumber'} );
356         C4::Letters::parseletter( $params->{'letter'}, 'items', $params->{'biblionumber'} );
357     }
358
359     return $params->{'letter'};
360 }
361
362 1;
363
364 __END__