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