Bug 18417: Advanced Editor (Rancor) add shortcuts for copyright symbols (C) (P)
[koha.git] / misc / cronjobs / advance_notices.pl
blob2274737aafb225a4894046d857d2c9765b366f5c
1 #!/usr/bin/perl
3 # Copyright 2008 LibLime
5 # This file is part of Koha.
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.
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.
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>.
20 =head1 NAME
22 advance_notices.pl - cron script to put item due reminders into message queue
24 =head1 SYNOPSIS
26 ./advance_notices.pl -c
28 or, in crontab:
29 0 1 * * * advance_notices.pl -c
31 =head1 DESCRIPTION
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.
39 =cut
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" };
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::Libraries;
61 use Koha::Patrons;
63 =head1 NAME
65 advance_notices.pl - prepare messages to be sent to patrons for nearly due, or due, items
67 =head1 SYNOPSIS
69 advance_notices.pl
70 [ -n ][ -m <number of days> ][ --itemscontent <comma separated field list> ][ -c ]
72 =head1 OPTIONS
74 =over 8
76 =item B<--help>
78 Print a brief help message and exits.
80 =item B<--man>
82 Prints the manual page and exits.
84 =item B<-v>
86 Verbose. Without this flag set, only fatal errors are reported.
88 =item B<-n>
90 Do not send any email. Advanced or due notices that would have been sent to
91 the patrons are printed to standard out.
93 =item B<-m>
95 Defines the maximum number of days in advance to send advance notices.
97 =item B<-c>
99 Confirm flag: Add this option. The script will only print a usage
100 statement otherwise.
102 =item B<--itemscontent>
104 comma separated list of fields that get substituted into templates in
105 places of the E<lt>E<lt>items.contentE<gt>E<gt> placeholder. This
106 defaults to date_due,title,author,barcode
108 Other possible values come from fields in the biblios, items and
109 issues tables.
111 =back
113 =head1 DESCRIPTION
115 This script is designed to alert patrons when items are due, or coming due
117 =head2 Configuration
119 This script pays attention to the advanced notice configuration
120 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
121 section of Koha is available in the Koha manual.
123 =head2 Outgoing emails
125 Typically, messages are prepared for each patron with due
126 items, and who have selected (or the library has elected for them) Advance or Due notices.
128 These emails are staged in the outgoing message queue, as are messages
129 produced by other features of Koha. This message queue must be
130 processed regularly by the
131 F<misc/cronjobs/process_message_queue.pl> program.
133 In the event that the C<-n> flag is passed to this program, no emails
134 are sent. Instead, messages are sent on standard output from this
135 program. They may be redirected to a file if desired.
137 =head2 Templates
139 Templates can contain variables enclosed in double angle brackets like
140 E<lt>E<lt>thisE<gt>E<gt>. Those variables will be replaced with values
141 specific to the overdue items or relevant patron. Available variables
142 are:
144 =over
146 =item E<lt>E<lt>items.contentE<gt>E<gt>
148 one line for each item, each line containing a tab separated list of
149 date due, title, author, barcode
151 =item E<lt>E<lt>borrowers.*E<gt>E<gt>
153 any field from the borrowers table
155 =item E<lt>E<lt>branches.*E<gt>E<gt>
157 any field from the branches table
159 =back
161 =head1 SEE ALSO
163 The F<misc/cronjobs/overdue_notices.pl> program allows you to send
164 messages to patrons when their messages are overdue.
165 =cut
167 binmode( STDOUT, ':encoding(UTF-8)' );
169 # These are defaults for command line options.
170 my $confirm; # -c: Confirm that the user has read and configured this script.
171 my $nomail; # -n: No mail. Will not send any emails.
172 my $mindays = 0; # -m: Maximum number of days in advance to send notices
173 my $maxdays = 30; # -e: the End of the time period
174 my $verbose = 0; # -v: verbose
175 my $itemscontent = join(',',qw( date_due title author barcode ));
177 my $help = 0;
178 my $man = 0;
180 GetOptions(
181 'help|?' => \$help,
182 'man' => \$man,
183 'c' => \$confirm,
184 'n' => \$nomail,
185 'm:i' => \$maxdays,
186 'v' => \$verbose,
187 'itemscontent=s' => \$itemscontent,
188 )or pod2usage(2);
189 pod2usage(1) if $help;
190 pod2usage( -verbose => 2 ) if $man;
192 # Since advance notice options are not visible in the web-interface
193 # unless EnhancedMessagingPreferences is on, let the user know that
194 # this script probably isn't going to do much
195 if ( ! C4::Context->preference('EnhancedMessagingPreferences') ) {
196 warn <<'END_WARN';
198 The "EnhancedMessagingPreferences" syspref is off.
199 Therefore, it is unlikely that this script will actually produce any messages to be sent.
200 To change this, edit the "EnhancedMessagingPreferences" syspref.
202 END_WARN
204 unless ($confirm) {
205 pod2usage(1);
208 cronlogaction();
210 # The fields that will be substituted into <<items.content>>
211 my @item_content_fields = split(/,/,$itemscontent);
213 warn 'getting upcoming due issues' if $verbose;
214 my $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $maxdays } );
215 warn 'found ' . scalar( @$upcoming_dues ) . ' issues' if $verbose;
217 # hash of borrowernumber to number of items upcoming
218 # for patrons wishing digests only.
219 my $upcoming_digest;
220 my $due_digest;
222 my $dbh = C4::Context->dbh();
223 my $sth = $dbh->prepare(<<'END_SQL');
224 SELECT biblio.*, items.*, issues.*
225 FROM issues,items,biblio
226 WHERE items.itemnumber=issues.itemnumber
227 AND biblio.biblionumber=items.biblionumber
228 AND issues.borrowernumber = ?
229 AND issues.itemnumber = ?
230 AND (TO_DAYS(date_due)-TO_DAYS(NOW()) = ?)
231 END_SQL
233 my $admin_adress = C4::Context->preference('KohaAdminEmailAddress');
235 my @letters;
236 UPCOMINGITEM: foreach my $upcoming ( @$upcoming_dues ) {
237 @letters = ();
238 warn 'examining ' . $upcoming->{'itemnumber'} . ' upcoming due items' if $verbose;
240 my $from_address = $upcoming->{branchemail} || $admin_adress;
242 my $borrower_preferences;
243 if ( 0 == $upcoming->{'days_until_due'} ) {
244 # This item is due today. Send an 'item due' message.
245 $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $upcoming->{'borrowernumber'},
246 message_name => 'item_due' } );
247 next unless $borrower_preferences;
249 if ( $borrower_preferences->{'wants_digest'} ) {
250 # cache this one to process after we've run through all of the items.
251 $due_digest->{ $upcoming->{borrowernumber} }->{email} = $from_address;
252 $due_digest->{ $upcoming->{borrowernumber} }->{count}++;
253 } else {
254 my $biblio = C4::Biblio::GetBiblioFromItemNumber( $upcoming->{'itemnumber'} );
255 my $letter_type = 'DUE';
256 $sth->execute($upcoming->{'borrowernumber'},$upcoming->{'itemnumber'},'0');
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";
263 ## Get branch info for borrowers home library.
264 foreach my $transport ( keys %{$borrower_preferences->{'transports'}} ) {
265 my $letter = parse_letter( { letter_code => $letter_type,
266 borrowernumber => $upcoming->{'borrowernumber'},
267 branchcode => $upcoming->{'branchcode'},
268 biblionumber => $biblio->{'biblionumber'},
269 itemnumber => $upcoming->{'itemnumber'},
270 substitute => { 'items.content' => $titles },
271 message_transport_type => $transport,
273 or warn "no letter of type '$letter_type' found for borrowernumber ".$upcoming->{'borrowernumber'}.". Please see sample_notices.sql";
274 push @letters, $letter if $letter;
277 } else {
278 $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $upcoming->{'borrowernumber'},
279 message_name => 'advance_notice' } );
280 next UPCOMINGITEM unless $borrower_preferences && exists $borrower_preferences->{'days_in_advance'};
281 next UPCOMINGITEM unless $borrower_preferences->{'days_in_advance'} == $upcoming->{'days_until_due'};
283 if ( $borrower_preferences->{'wants_digest'} ) {
284 # cache this one to process after we've run through all of the items.
285 $upcoming_digest->{ $upcoming->{borrowernumber} }->{email} = $from_address;
286 $upcoming_digest->{ $upcoming->{borrowernumber} }->{count}++;
287 } else {
288 my $biblio = C4::Biblio::GetBiblioFromItemNumber( $upcoming->{'itemnumber'} );
289 my $letter_type = 'PREDUE';
290 $sth->execute($upcoming->{'borrowernumber'},$upcoming->{'itemnumber'},$borrower_preferences->{'days_in_advance'});
291 my $titles = "";
292 while ( my $item_info = $sth->fetchrow_hashref()) {
293 my @item_info = map { $_ =~ /^date|date$/ ? format_date($item_info->{$_}) : $item_info->{$_} || '' } @item_content_fields;
294 $titles .= join("\t",@item_info) . "\n";
297 ## Get branch info for borrowers home library.
298 foreach my $transport ( keys %{$borrower_preferences->{'transports'}} ) {
299 my $letter = parse_letter( { letter_code => $letter_type,
300 borrowernumber => $upcoming->{'borrowernumber'},
301 branchcode => $upcoming->{'branchcode'},
302 biblionumber => $biblio->{'biblionumber'},
303 itemnumber => $upcoming->{'itemnumber'},
304 substitute => { 'items.content' => $titles },
305 message_transport_type => $transport,
307 or warn "no letter of type '$letter_type' found for borrowernumber ".$upcoming->{'borrowernumber'}.". Please see sample_notices.sql";
308 push @letters, $letter if $letter;
313 # If we have prepared a letter, send it.
314 if ( @letters ) {
315 if ($nomail) {
316 for my $letter ( @letters ) {
317 local $, = "\f";
318 print $letter->{'content'};
321 else {
322 for my $letter ( @letters ) {
323 C4::Letters::EnqueueLetter( { letter => $letter,
324 borrowernumber => $upcoming->{'borrowernumber'},
325 from_address => $from_address,
326 message_transport_type => $letter->{message_transport_type} } );
334 # Now, run through all the people that want digests and send them
336 $sth = $dbh->prepare(<<'END_SQL');
337 SELECT biblio.*, items.*, issues.*
338 FROM issues,items,biblio
339 WHERE items.itemnumber=issues.itemnumber
340 AND biblio.biblionumber=items.biblionumber
341 AND issues.borrowernumber = ?
342 AND (TO_DAYS(date_due)-TO_DAYS(NOW()) = ?)
343 END_SQL
344 PATRON: while ( my ( $borrowernumber, $digest ) = each %$upcoming_digest ) {
345 @letters = ();
346 my $count = $digest->{count};
347 my $from_address = $digest->{email};
349 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber,
350 message_name => 'advance_notice' } );
351 next PATRON unless $borrower_preferences; # how could this happen?
354 my $letter_type = 'PREDUEDGST';
356 $sth->execute($borrowernumber,$borrower_preferences->{'days_in_advance'});
357 my $titles = "";
358 while ( my $item_info = $sth->fetchrow_hashref()) {
359 my @item_info = map { $_ =~ /^date|date$/ ? format_date($item_info->{$_}) : $item_info->{$_} || '' } @item_content_fields;
360 $titles .= join("\t",@item_info) . "\n";
363 ## Get branch info for borrowers home library.
364 my %branch_info = get_branch_info( $borrowernumber );
366 foreach my $transport ( keys %{ $borrower_preferences->{'transports'} } ) {
367 my $letter = parse_letter(
369 letter_code => $letter_type,
370 borrowernumber => $borrowernumber,
371 substitute => {
372 count => $count,
373 'items.content' => $titles,
374 %branch_info,
376 branchcode => $branch_info{"branches.branchcode"},
377 message_transport_type => $transport,
380 or warn "no letter of type '$letter_type' found for borrowernumber $borrowernumber. Please see sample_notices.sql";
381 push @letters, $letter if $letter;
384 if ( @letters ) {
385 if ($nomail) {
386 for my $letter ( @letters ) {
387 local $, = "\f";
388 print $letter->{'content'};
391 else {
392 for my $letter ( @letters ) {
393 C4::Letters::EnqueueLetter( { letter => $letter,
394 borrowernumber => $borrowernumber,
395 from_address => $from_address,
396 message_transport_type => $letter->{message_transport_type} } );
402 # Now, run through all the people that want digests and send them
403 PATRON: while ( my ( $borrowernumber, $digest ) = each %$due_digest ) {
404 @letters = ();
405 my $count = $digest->{count};
406 my $from_address = $digest->{email};
408 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber,
409 message_name => 'item_due' } );
410 next PATRON unless $borrower_preferences; # how could this happen?
412 my $letter_type = 'DUEDGST';
413 $sth->execute($borrowernumber,'0');
414 my $titles = "";
415 while ( my $item_info = $sth->fetchrow_hashref()) {
416 my @item_info = map { $_ =~ /^date|date$/ ? format_date($item_info->{$_}) : $item_info->{$_} || '' } @item_content_fields;
417 $titles .= join("\t",@item_info) . "\n";
420 ## Get branch info for borrowers home library.
421 my %branch_info = get_branch_info( $borrowernumber );
423 for my $transport ( keys %{ $borrower_preferences->{'transports'} } ) {
424 my $letter = parse_letter(
426 letter_code => $letter_type,
427 borrowernumber => $borrowernumber,
428 substitute => {
429 count => $count,
430 'items.content' => $titles,
431 %branch_info,
433 branchcode => $branch_info{"branches.branchcode"},
434 message_transport_type => $transport,
437 or warn "no letter of type '$letter_type' found for borrowernumber $borrowernumber. Please see sample_notices.sql";
438 push @letters, $letter if $letter;
441 if ( @letters ) {
442 if ($nomail) {
443 for my $letter ( @letters ) {
444 local $, = "\f";
445 print $letter->{'content'};
448 else {
449 for my $letter ( @letters ) {
450 C4::Letters::EnqueueLetter( { letter => $letter,
451 borrowernumber => $borrowernumber,
452 from_address => $from_address,
453 message_transport_type => $letter->{message_transport_type} } );
460 =head1 METHODS
462 =head2 parse_letter
464 =cut
466 sub parse_letter {
467 my $params = shift;
468 foreach my $required ( qw( letter_code borrowernumber ) ) {
469 return unless exists $params->{$required};
471 my $patron = Koha::Patrons->find( $params->{borrowernumber} );
473 my %table_params = ( 'borrowers' => $params->{'borrowernumber'} );
475 if ( my $p = $params->{'branchcode'} ) {
476 $table_params{'branches'} = $p;
478 if ( my $p = $params->{'itemnumber'} ) {
479 $table_params{'issues'} = $p;
480 $table_params{'items'} = $p;
482 if ( my $p = $params->{'biblionumber'} ) {
483 $table_params{'biblio'} = $p;
484 $table_params{'biblioitems'} = $p;
487 return C4::Letters::GetPreparedLetter (
488 module => 'circulation',
489 letter_code => $params->{'letter_code'},
490 branchcode => $table_params{'branches'},
491 lang => $patron->lang,
492 substitute => $params->{'substitute'},
493 tables => \%table_params,
494 message_transport_type => $params->{message_transport_type},
498 sub format_date {
499 my $date_string = shift;
500 my $dt=dt_from_string($date_string);
501 return output_pref($dt);
504 =head2 get_branch_info
506 =cut
508 sub get_branch_info {
509 my ( $borrowernumber ) = @_;
511 ## Get branch info for borrowers home library.
512 my $borrower_details = C4::Members::GetMember( borrowernumber => $borrowernumber );
513 my $borrower_branchcode = $borrower_details->{'branchcode'};
514 my $branch = Koha::Libraries->find( $borrower_branchcode )->unblessed;
515 my %branch_info;
516 foreach my $key( keys %$branch ) {
517 $branch_info{"branches.$key"} = $branch->{$key};
520 return %branch_info;
525 __END__