Bug 21886: Add option to send notices from owning library instead of issuing library
[koha.git] / misc / cronjobs / overdue_notices.pl
1 #!/usr/bin/perl
2
3 # Copyright 2008 Liblime
4 # Copyright 2010 BibLibre
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20
21 use Modern::Perl;
22
23 BEGIN {
24
25     # find Koha's Perl modules
26     # test carefully before changing this
27     use FindBin;
28     eval { require "$FindBin::Bin/../kohalib.pl" };
29 }
30
31 use Getopt::Long;
32 use Pod::Usage;
33 use Text::CSV_XS;
34 use DateTime;
35 use DateTime::Duration;
36
37 use Koha::Script -cron;
38 use C4::Context;
39 use C4::Letters;
40 use C4::Overdues qw(GetFine GetOverdueMessageTransportTypes parse_overdues_letter);
41 use C4::Log;
42 use Koha::Patron::Debarments qw(AddUniqueDebarment);
43 use Koha::DateUtils;
44 use Koha::Calendar;
45 use Koha::Libraries;
46 use Koha::Acquisition::Currencies;
47 use Koha::Patrons;
48
49 =head1 NAME
50
51 overdue_notices.pl - prepare messages to be sent to patrons for overdue items
52
53 =head1 SYNOPSIS
54
55 overdue_notices.pl
56   [ -n ][ -library <branchcode> ][ -library <branchcode> ... ]
57   [ -max <number of days> ][ -csv [<filename>] ][ -itemscontent <field list> ]
58   [ -email <email_type> ... ]
59
60  Options:
61    -help                          Brief help message.
62    -man                           Full documentation.
63    -v                             Verbose mode.
64    -n                             No email will be sent.
65    -max          <days>           Maximum days overdue to deal with.
66    -library      <branchcode>     Only deal with overdues from this library.
67                                   (repeatable : several libraries can be given)
68    -csv          <filename>       Populate CSV file.
69    -html         <directory>      Output html to a file in the given directory.
70    -text         <directory>      Output plain text to a file in the given directory.
71    -itemscontent <list of fields> Item information in templates.
72    -borcat       <categorycode>   Category code that must be included.
73    -borcatout    <categorycode>   Category code that must be excluded.
74    -t                             Only include triggered overdues.
75    --test                         Run in test mode. No changes will be made on the DB.
76    -list-all                      List all overdues.
77    -date         <yyyy-mm-dd>     Emulate overdues run for this date.
78    -email        <email_type>     Type of email that will be used.
79                                   Can be 'email', 'emailpro' or 'B_email'. Repeatable.
80    --owning                       Send notices from item homebranch library instead of issuing library
81
82 =head1 OPTIONS
83
84 =over 8
85
86 =item B<-help>
87
88 Print a brief help message and exits.
89
90 =item B<-man>
91
92 Prints the manual page and exits.
93
94 =item B<-v>
95
96 Verbose. Without this flag set, only fatal errors are reported.
97
98 =item B<-n>
99
100 Do not send any email. Overdue notices that would have been sent to
101 the patrons or to the admin are printed to standard out. CSV data (if
102 the -csv flag is set) is written to standard out or to any csv
103 filename given.
104
105 =item B<-max>
106
107 Items older than max days are assumed to be handled somewhere else,
108 probably the F<longoverdues.pl> script. They are therefore ignored by
109 this program. No notices are sent for them, and they are not added to
110 any CSV files. Defaults to 90 to match F<longoverdues.pl>.
111
112 =item B<-library>
113
114 select overdues for one specific library. Use the value in the
115 branches.branchcode table. This option can be repeated in order 
116 to select overdues for a group of libraries.
117
118 =item B<-csv>
119
120 Produces CSV data. if -n (no mail) flag is set, then this CSV data is
121 sent to standard out or to a filename if provided. Otherwise, only
122 overdues that could not be emailed are sent in CSV format to the admin.
123
124 =item B<-html>
125
126 Produces html data. If patron does not have an email address or
127 -n (no mail) flag is set, an HTML file is generated in the specified
128 directory. This can be downloaded or further processed by library staff.
129 The file will be called notices-YYYY-MM-DD.html and placed in the directory
130 specified.
131
132 =item B<-text>
133
134 Produces plain text data. If patron does not have an email address or
135 -n (no mail) flag is set, a text file is generated in the specified
136 directory. This can be downloaded or further processed by library staff.
137 The file will be called notices-YYYY-MM-DD.txt and placed in the directory
138 specified.
139
140 =item B<-itemscontent>
141
142 comma separated list of fields that get substituted into templates in
143 places of the E<lt>E<lt>items.contentE<gt>E<gt> placeholder. This
144 defaults to due date,title,barcode,author
145
146 Other possible values come from fields in the biblios, items and
147 issues tables.
148
149 =item B<-borcat>
150
151 Repeatable field, that permits to select only some patron categories.
152
153 =item B<-borcatout>
154
155 Repeatable field, that permits to exclude some patron categories.
156
157 =item B<-t> | B<--triggered>
158
159 This option causes a notice to be generated if and only if 
160 an item is overdue by the number of days defined in a notice trigger.
161
162 By default, a notice is sent each time the script runs, which is suitable for 
163 less frequent run cron script, but requires syncing notice triggers with 
164 the  cron schedule to ensure proper behavior.
165 Add the --triggered option for daily cron, at the risk of no notice 
166 being generated if the cron fails to run on time.
167
168 =item B<-test>
169
170 This option makes the script run in test mode.
171
172 In test mode, the script won't make any changes on the DB. This is useful
173 for debugging configuration.
174
175 =item B<-list-all>
176
177 Default items.content lists only those items that fall in the 
178 range of the currently processing notice.
179 Choose list-all to include all overdue items in the list (limited by B<-max> setting).
180
181 =item B<-date>
182
183 use it in order to send overdues on a specific date and not Now. Format: YYYY-MM-DD.
184
185 =item B<-email>
186
187 Allows to specify which type of email will be used. Can be email, emailpro or B_email. Repeatable.
188
189 =item B<--owning>
190
191 Use the address information from the item homebranch library instead of the issuing library.
192
193 =back
194
195 =head1 DESCRIPTION
196
197 This script is designed to alert patrons and administrators of overdue
198 items.
199
200 =head2 Configuration
201
202 This script pays attention to the overdue notice configuration
203 performed in the "Overdue notice/status triggers" section of the
204 "Tools" area of the staff interface to Koha. There, you can choose
205 which letter templates are sent out after a configurable number of
206 days to patrons of each library. More information about the use of this
207 section of Koha is available in the Koha manual.
208
209 The templates used to craft the emails are defined in the "Tools:
210 Notices" section of the staff interface to Koha.
211
212 =head2 Outgoing emails
213
214 Typically, messages are prepared for each patron with overdue
215 items. Messages for whom there is no email address on file are
216 collected and sent as attachments in a single email to each library
217 administrator, or if that is not set, then to the email address in the
218 C<KohaAdminEmailAddress> system preference.
219
220 These emails are staged in the outgoing message queue, as are messages
221 produced by other features of Koha. This message queue must be
222 processed regularly by the
223 F<misc/cronjobs/process_message_queue.pl> program.
224
225 In the event that the C<-n> flag is passed to this program, no emails
226 are sent. Instead, messages are sent on standard output from this
227 program. They may be redirected to a file if desired.
228
229 =head2 Templates
230
231 Templates can contain variables enclosed in double angle brackets like
232 E<lt>E<lt>thisE<gt>E<gt>. Those variables will be replaced with values
233 specific to the overdue items or relevant patron. Available variables
234 are:
235
236 =over
237
238 =item E<lt>E<lt>bibE<gt>E<gt>
239
240 the name of the library
241
242 =item E<lt>E<lt>items.contentE<gt>E<gt>
243
244 one line for each item, each line containing a tab separated list of
245 title, author, barcode, issuedate
246
247 =item E<lt>E<lt>borrowers.*E<gt>E<gt>
248
249 any field from the borrowers table
250
251 =item E<lt>E<lt>branches.*E<gt>E<gt>
252
253 any field from the branches table
254
255 =back
256
257 =head2 CSV output
258
259 The C<-csv> command line option lets you specify a file to which
260 overdues data should be output in CSV format.
261
262 With the C<-n> flag set, data about all overdues is written to the
263 file. Without that flag, only information about overdues that were
264 unable to be sent directly to the patrons will be written. In other
265 words, this CSV file replaces the data that is typically sent to the
266 administrator email address.
267
268 =head1 USAGE EXAMPLES
269
270 C<overdue_notices.pl> - In this most basic usage, with no command line
271 arguments, all libraries are processed individually, and notices are
272 prepared for all patrons with overdue items for whom we have email
273 addresses. Messages for those patrons for whom we have no email
274 address are sent in a single attachment to the library administrator's
275 email address, or to the address in the KohaAdminEmailAddress system
276 preference.
277
278 C<overdue_notices.pl -n -csv /tmp/overdues.csv> - sends no email and
279 populates F</tmp/overdues.csv> with information about all overdue
280 items.
281
282 C<overdue_notices.pl -library MAIN max 14> - prepare notices of
283 overdues in the last 2 weeks for the MAIN library.
284
285 =head1 SEE ALSO
286
287 The F<misc/cronjobs/advance_notices.pl> program allows you to send
288 messages to patrons in advance of their items becoming due, or to
289 alert them of items that have just become due.
290
291 =cut
292
293 # These variables are set by command line options.
294 # They are initially set to default values.
295 my $dbh = C4::Context->dbh();
296 my $help    = 0;
297 my $man     = 0;
298 my $verbose = 0;
299 my $nomail  = 0;
300 my $MAX     = 90;
301 my $test_mode = 0;
302 my $owning_library = 0;
303 my @branchcodes; # Branch(es) passed as parameter
304 my @emails_to_use;    # Emails to use for messaging
305 my @emails;           # Emails given in command-line parameters
306 my $csvfilename;
307 my $htmlfilename;
308 my $text_filename;
309 my $triggered = 0;
310 my $listall = 0;
311 my $itemscontent = join( ',', qw( date_due title barcode author itemnumber ) );
312 my @myborcat;
313 my @myborcatout;
314 my ( $date_input, $today );
315
316 GetOptions(
317     'help|?'         => \$help,
318     'man'            => \$man,
319     'v'              => \$verbose,
320     'n'              => \$nomail,
321     'max=s'          => \$MAX,
322     'library=s'      => \@branchcodes,
323     'csv:s'          => \$csvfilename,    # this optional argument gets '' if not supplied.
324     'html:s'         => \$htmlfilename,    # this optional argument gets '' if not supplied.
325     'text:s'         => \$text_filename,    # this optional argument gets '' if not supplied.
326     'itemscontent=s' => \$itemscontent,
327     'list-all'       => \$listall,
328     't|triggered'    => \$triggered,
329     'test'           => \$test_mode,
330     'date=s'         => \$date_input,
331     'borcat=s'       => \@myborcat,
332     'borcatout=s'    => \@myborcatout,
333     'email=s'        => \@emails,
334     'owning'         => \$owning_library,
335 ) or pod2usage(2);
336 pod2usage(1) if $help;
337 pod2usage( -verbose => 2 ) if $man;
338 cronlogaction() unless $test_mode;
339
340 if ( defined $csvfilename && $csvfilename =~ /^-/ ) {
341     warn qq(using "$csvfilename" as filename, that seems odd);
342 }
343
344 my @overduebranches    = C4::Overdues::GetBranchcodesWithOverdueRules();        # Branches with overdue rules
345 my @branches;                                                                   # Branches passed as parameter with overdue rules
346 my $branchcount = scalar(@overduebranches);
347
348 my $overduebranch_word = scalar @overduebranches > 1 ? 'branches' : 'branch';
349 my $branchcodes_word = scalar @branchcodes > 1 ? 'branches' : 'branch';
350
351 my $PrintNoticesMaxLines = C4::Context->preference('PrintNoticesMaxLines');
352
353 if ($branchcount) {
354     $verbose and warn "Found $branchcount $overduebranch_word with first message enabled: " . join( ', ', map { "'$_'" } @overduebranches ), "\n";
355 } else {
356     die 'No branches with active overduerules';
357 }
358
359 if (@branchcodes) {
360     $verbose and warn "$branchcodes_word @branchcodes passed on parameter\n";
361     
362     # Getting libraries which have overdue rules
363     my %seen = map { $_ => 1 } @branchcodes;
364     @branches = grep { $seen{$_} } @overduebranches;
365     
366     
367     if (@branches) {
368
369         my $branch_word = scalar @branches > 1 ? 'branches' : 'branch';
370         $verbose and warn "$branch_word @branches have overdue rules\n";
371
372     } else {
373     
374         $verbose and warn "No active overduerules for $branchcodes_word  '@branchcodes'\n";
375         ( scalar grep { '' eq $_ } @branches )
376           or die "No active overduerules for DEFAULT either!";
377         $verbose and warn "Falling back on default rules for @branchcodes\n";
378         @branches = ('');
379     }
380 }
381 my $date_to_run;
382 my $date;
383 if ( $date_input ){
384     eval {
385         $date_to_run = dt_from_string( $date_input, 'iso' );
386     };
387     die "$date_input is not a valid date, aborting! Use a date in format YYYY-MM-DD."
388         if $@ or not $date_to_run;
389
390     # It's certainly useless to escape $date_input
391     # dt_from_string should not return something if $date_input is not correctly set.
392     $date = $dbh->quote( $date_input );
393 }
394 else {
395     $date="NOW()";
396     $date_to_run = dt_from_string();
397 }
398
399 # these are the fields that will be substituted into <<item.content>>
400 my @item_content_fields = split( /,/, $itemscontent );
401
402 binmode( STDOUT, ':encoding(UTF-8)' );
403
404
405 our $csv;       # the Text::CSV_XS object
406 our $csv_fh;    # the filehandle to the CSV file.
407 if ( defined $csvfilename ) {
408     my $sep_char = C4::Context->preference('delimiter') || ';';
409     $sep_char = "\t" if ($sep_char eq 'tabulation');
410     $csv = Text::CSV_XS->new( { binary => 1 , sep_char => $sep_char } );
411     if ( $csvfilename eq '' ) {
412         $csv_fh = *STDOUT;
413     } else {
414         open $csv_fh, ">", $csvfilename or die "unable to open $csvfilename: $!";
415     }
416     if ( $csv->combine(qw(name surname address1 address2 zipcode city country email phone cardnumber itemcount itemsinfo branchname letternumber)) ) {
417         print $csv_fh $csv->string, "\n";
418     } else {
419         $verbose and warn 'combine failed on argument: ' . $csv->error_input;
420     }
421 }
422
423 @branches = @overduebranches unless @branches;
424 our $fh;
425 if ( defined $htmlfilename ) {
426   if ( $htmlfilename eq '' ) {
427     $fh = *STDOUT;
428   } else {
429     my $today = dt_from_string();
430     open $fh, ">:encoding(UTF-8)",File::Spec->catdir ($htmlfilename,"notices-".$today->ymd().".html");
431   }
432   
433   print $fh "<html>\n";
434   print $fh "<head>\n";
435   print $fh "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n";
436   print $fh "<style type='text/css'>\n";
437   print $fh "pre {page-break-after: always;}\n";
438   print $fh "pre {white-space: pre-wrap;}\n";
439   print $fh "pre {white-space: -moz-pre-wrap;}\n";
440   print $fh "pre {white-space: -o-pre-wrap;}\n";
441   print $fh "pre {word-wrap: break-work;}\n";
442   print $fh "</style>\n";
443   print $fh "</head>\n";
444   print $fh "<body>\n";
445 }
446 elsif ( defined $text_filename ) {
447   if ( $text_filename eq '' ) {
448     $fh = *STDOUT;
449   } else {
450     my $today = dt_from_string();
451     open $fh, ">:encoding(UTF-8)",File::Spec->catdir ($text_filename,"notices-".$today->ymd().".txt");
452   }
453 }
454
455 foreach my $branchcode (@branches) {
456     my $calendar;
457     if ( C4::Context->preference('OverdueNoticeCalendar') ) {
458         $calendar = Koha::Calendar->new( branchcode => $branchcode );
459         if ( $calendar->is_holiday($date_to_run) ) {
460             next;
461         }
462     }
463
464     my $library             = Koha::Libraries->find($branchcode);
465     my $admin_email_address = $library->branchemail
466       || C4::Context->preference('KohaAdminEmailAddress');
467     my $branch_email_address = C4::Context->preference('AddressForFailedOverdueNotices')
468       || $library->inbound_email_address;
469     my @output_chunks;    # may be sent to mail or stdout or csv file.
470
471     $verbose and warn sprintf "branchcode : '%s' using %s\n", $branchcode, $branch_email_address;
472
473     my $sql2 = <<"END_SQL";
474 SELECT biblio.*, items.*, issues.*, biblioitems.itemtype, branchname
475   FROM issues,items,biblio, biblioitems, branches b
476   WHERE items.itemnumber=issues.itemnumber
477     AND biblio.biblionumber   = items.biblionumber
478     AND b.branchcode = items.homebranch
479     AND biblio.biblionumber   = biblioitems.biblionumber
480     AND issues.borrowernumber = ?
481     AND items.itemlost = 0
482     AND TO_DAYS($date)-TO_DAYS(issues.date_due) >= 0
483 END_SQL
484
485     if($owning_library) {
486       $sql2 .= ' AND items.homebranch = ? ';
487     } else {
488       $sql2 .= ' AND issues.branchcode = ? ';
489     }
490     my $sth2 = $dbh->prepare($sql2);
491
492     my $query = "SELECT * FROM overduerules WHERE delay1 IS NOT NULL AND branchcode = ? ";
493     $query .= " AND categorycode IN (".join( ',' , ('?') x @myborcat ).") " if (@myborcat);
494     $query .= " AND categorycode NOT IN (".join( ',' , ('?') x @myborcatout ).") " if (@myborcatout);
495     
496     my $rqoverduerules =  $dbh->prepare($query);
497     $rqoverduerules->execute($branchcode, @myborcat, @myborcatout);
498     
499     # We get default rules is there is no rule for this branch
500     if($rqoverduerules->rows == 0){
501         $query = "SELECT * FROM overduerules WHERE delay1 IS NOT NULL AND branchcode = '' ";
502         $query .= " AND categorycode IN (".join( ',' , ('?') x @myborcat ).") " if (@myborcat);
503         $query .= " AND categorycode NOT IN (".join( ',' , ('?') x @myborcatout ).") " if (@myborcatout);
504         
505         $rqoverduerules = $dbh->prepare($query);
506         $rqoverduerules->execute(@myborcat, @myborcatout);
507     }
508
509     # my $outfile = 'overdues_' . ( $mybranch || $branchcode || 'default' );
510     while ( my $overdue_rules = $rqoverduerules->fetchrow_hashref ) {
511       PERIOD: foreach my $i ( 1 .. 3 ) {
512
513             $verbose and warn "branch '$branchcode', categorycode = $overdue_rules->{categorycode} pass $i\n";
514
515             my $mindays = $overdue_rules->{"delay$i"};    # the notice will be sent after mindays days (grace period)
516             my $maxdays = (
517                   $overdue_rules->{ "delay" . ( $i + 1 ) }
518                 ? $overdue_rules->{ "delay" . ( $i + 1 ) } - 1
519                 : ($MAX)
520             );                                            # issues being more than maxdays late are managed somewhere else. (borrower probably suspended)
521
522             next unless defined $mindays;
523
524             if ( !$overdue_rules->{"letter$i"} ) {
525                 $verbose and warn "No letter$i code for branch '$branchcode'";
526                 next PERIOD;
527             }
528
529             # $letter->{'content'} is the text of the mail that is sent.
530             # this text contains fields that are replaced by their value. Those fields must be written between brackets
531             # The following fields are available :
532             # itemcount is interpreted here as the number of items in the overdue range defined by the current notice or all overdues < max if(-list-all).
533             # <date> <itemcount> <firstname> <lastname> <address1> <address2> <address3> <city> <postcode> <country>
534
535             my $borrower_sql = <<"END_SQL";
536 SELECT issues.borrowernumber, firstname, surname, address, address2, city, zipcode, country, email, emailpro, B_email, smsalertnumber, phone, cardnumber, date_due
537 FROM   issues,borrowers,categories,items
538 WHERE  issues.borrowernumber=borrowers.borrowernumber
539 AND    borrowers.categorycode=categories.categorycode
540 AND    issues.itemnumber = items.itemnumber
541 AND    items.itemlost = 0
542 AND    TO_DAYS($date)-TO_DAYS(issues.date_due) >= 0
543 END_SQL
544             my @borrower_parameters;
545             if ($branchcode) {
546                 if($owning_library) {
547                     $borrower_sql .= ' AND items.homebranch=? ';
548                 } else {
549                     $borrower_sql .= ' AND issues.branchcode=? ';
550                 }
551                 push @borrower_parameters, $branchcode;
552             }
553             if ( $overdue_rules->{categorycode} ) {
554                 $borrower_sql .= ' AND borrowers.categorycode=? ';
555                 push @borrower_parameters, $overdue_rules->{categorycode};
556             }
557             $borrower_sql .= '  AND categories.overduenoticerequired=1 ORDER BY issues.borrowernumber';
558
559             # $sth gets borrower info iff at least one overdue item has triggered the overdue action.
560                 my $sth = $dbh->prepare($borrower_sql);
561             $sth->execute(@borrower_parameters);
562
563             $verbose and warn $borrower_sql . "\n $branchcode | " . $overdue_rules->{'categorycode'} . "\n ($mindays, $maxdays, ".  $date_to_run->datetime() .")\nreturns " . $sth->rows . " rows";
564             my $borrowernumber;
565             while ( my $data = $sth->fetchrow_hashref ) {
566
567                 # check the borrower has at least one item that matches
568                 my $days_between;
569                 if ( C4::Context->preference('OverdueNoticeCalendar') )
570                 {
571                     $days_between =
572                       $calendar->days_between( dt_from_string($data->{date_due}),
573                         $date_to_run );
574                 }
575                 else {
576                     $days_between =
577                       $date_to_run->delta_days( dt_from_string($data->{date_due}) );
578                 }
579                 $days_between = $days_between->in_units('days');
580                 if ($triggered) {
581                     if ( $mindays != $days_between ) {
582                         next;
583                     }
584                 }
585                 else {
586                     unless (   $days_between >= $mindays
587                         && $days_between <= $maxdays )
588                     {
589                         next;
590                     }
591                 }
592                 if (defined $borrowernumber && $borrowernumber eq $data->{'borrowernumber'}){
593 # we have already dealt with this borrower
594                     $verbose and warn "already dealt with this borrower $borrowernumber";
595                     next;
596                 }
597                 $borrowernumber = $data->{'borrowernumber'};
598                 my $borr = sprintf( "%s%s%s (%s)",
599                     $data->{'surname'} || '',
600                     $data->{'firstname'} && $data->{'surname'} ? ', ' : '',
601                     $data->{'firstname'} || '',
602                     $borrowernumber );
603                 $verbose
604                   and warn "borrower $borr has items triggering level $i.";
605
606                 my $patron = Koha::Patrons->find( $borrowernumber );
607                 @emails_to_use = ();
608                 my $notice_email = $patron->notice_email_address;
609                 unless ($nomail) {
610                     if (@emails) {
611                         foreach (@emails) {
612                             push @emails_to_use, $data->{$_} if ( $data->{$_} );
613                         }
614                     }
615                     else {
616                         push @emails_to_use, $notice_email if ($notice_email);
617                     }
618                 }
619
620                 my $letter = C4::Letters::getletter( 'circulation', $overdue_rules->{"letter$i"}, $branchcode, undef, $patron->lang );
621
622                 unless ($letter) {
623                     $verbose and warn qq|Message '$overdue_rules->{"letter$i"}' content not found|;
624
625                     # might as well skip while PERIOD, no other borrowers are going to work.
626                     # FIXME : Does this mean a letter must be defined in order to trigger a debar ?
627                     next PERIOD;
628                 }
629     
630                 if ( $overdue_rules->{"debarred$i"} ) {
631     
632                     #action taken is debarring
633                     AddUniqueDebarment(
634                         {
635                             borrowernumber => $borrowernumber,
636                             type           => 'OVERDUES',
637                             comment => "OVERDUES_PROCESS " .  output_pref( dt_from_string() ),
638                         }
639                     ) unless $test_mode;
640                     $verbose and warn "debarring $borr\n";
641                 }
642                 my @params = ($borrowernumber,$branchcode);
643                 $verbose and warn "STH2 PARAMS: borrowernumber = $borrowernumber";
644
645                 $sth2->execute(@params);
646                 my $itemcount = 0;
647                 my $titles = "";
648                 my @items = ();
649                 
650                 my $j = 0;
651                 my $exceededPrintNoticesMaxLines = 0;
652                 while ( my $item_info = $sth2->fetchrow_hashref() ) {
653                     if ( C4::Context->preference('OverdueNoticeCalendar') ) {
654                         $days_between =
655                           $calendar->days_between(
656                             dt_from_string( $item_info->{date_due} ), $date_to_run );
657                     }
658                     else {
659                         $days_between =
660                           $date_to_run->delta_days(
661                             dt_from_string( $item_info->{date_due} ) );
662                     }
663                     $days_between = $days_between->in_units('days');
664                     if ($listall){
665                         unless ($days_between >= 1 and $days_between <= $MAX){
666                             next;
667                         }
668                     }
669                     else {
670                         if ($triggered) {
671                             if ( $mindays != $days_between ) {
672                                 next;
673                             }
674                         }
675                         else {
676                             unless ( $days_between >= $mindays
677                                 && $days_between <= $maxdays )
678                             {
679                                 next;
680                             }
681                         }
682                     }
683
684                     if ( ( scalar(@emails_to_use) == 0 || $nomail ) && $PrintNoticesMaxLines && $j >= $PrintNoticesMaxLines ) {
685                       $exceededPrintNoticesMaxLines = 1;
686                       last;
687                     }
688                     $j++;
689
690                     $titles .= C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields, dateonly => 1 } );
691                     $itemcount++;
692                     push @items, $item_info;
693                 }
694                 $sth2->finish;
695
696                 my @message_transport_types = @{ GetOverdueMessageTransportTypes( $branchcode, $overdue_rules->{categorycode}, $i) };
697                 @message_transport_types = @{ GetOverdueMessageTransportTypes( q{}, $overdue_rules->{categorycode}, $i) }
698                     unless @message_transport_types;
699
700
701                 my $print_sent = 0; # A print notice is not yet sent for this patron
702                 for my $mtt ( @message_transport_types ) {
703                     my $effective_mtt = $mtt;
704                     if ( ($mtt eq 'email' and not scalar @emails_to_use) or ($mtt eq 'sms' and not $data->{smsalertnumber}) ) {
705                         # email or sms is requested but not exist, do a print.
706                         $effective_mtt = 'print';
707                     }
708                     splice @items, $PrintNoticesMaxLines if $effective_mtt eq 'print' && $PrintNoticesMaxLines && scalar @items > $PrintNoticesMaxLines;
709                     #catch the case where we are sending a print to someone with an email
710
711                     my $letter_exists = C4::Letters::getletter( 'circulation', $overdue_rules->{"letter$i"}, $branchcode, $effective_mtt, $patron->lang ) ? 1 : 0;
712                     my $letter = parse_overdues_letter(
713                         {   letter_code     => $overdue_rules->{"letter$i"},
714                             borrowernumber  => $borrowernumber,
715                             branchcode      => $branchcode,
716                             items           => \@items,
717                             substitute      => {    # this appears to be a hack to overcome incomplete features in this code.
718                                                 bib             => $library->branchname, # maybe 'bib' is a typo for 'lib<rary>'?
719                                                 'items.content' => $titles,
720                                                 'count'         => $itemcount,
721                                                },
722                             # If there is no template defined for the requested letter
723                             # Fallback on the original type
724                             message_transport_type => $letter_exists ? $effective_mtt : $mtt,
725                         }
726                     );
727                     unless ($letter) {
728                         $verbose and warn qq|Message '$overdue_rules->{"letter$i"}' content not found|;
729                         # this transport doesn't have a configured notice, so try another
730                         next;
731                     }
732
733                     if ( $exceededPrintNoticesMaxLines ) {
734                       $letter->{'content'} .= "List too long for form; please check your account online for a complete list of your overdue items.";
735                     }
736
737                     my @misses = grep { /./ } map { /^([^>]*)[>]+/; ( $1 || '' ); } split /\</, $letter->{'content'};
738                     if (@misses) {
739                         $verbose and warn "The following terms were not matched and replaced: \n\t" . join "\n\t", @misses;
740                     }
741
742                     if ($nomail) {
743                         push @output_chunks,
744                           prepare_letter_for_printing(
745                           {   letter         => $letter,
746                               borrowernumber => $borrowernumber,
747                               firstname      => $data->{'firstname'},
748                               lastname       => $data->{'surname'},
749                               address1       => $data->{'address'},
750                               address2       => $data->{'address2'},
751                               city           => $data->{'city'},
752                               phone          => $data->{'phone'},
753                               cardnumber     => $data->{'cardnumber'},
754                               branchname     => $library->branchname,
755                               letternumber   => $i,
756                               postcode       => $data->{'zipcode'},
757                               country        => $data->{'country'},
758                               email          => $notice_email,
759                               itemcount      => $itemcount,
760                               titles         => $titles,
761                               outputformat   => defined $csvfilename ? 'csv' : defined $htmlfilename ? 'html' : defined $text_filename ? 'text' : '',
762                             }
763                           );
764                     } else {
765                         if ( ($mtt eq 'email' and not scalar @emails_to_use) or ($mtt eq 'sms' and not $data->{smsalertnumber}) ) {
766                             push @output_chunks,
767                               prepare_letter_for_printing(
768                               {   letter         => $letter,
769                                   borrowernumber => $borrowernumber,
770                                   firstname      => $data->{'firstname'},
771                                   lastname       => $data->{'surname'},
772                                   address1       => $data->{'address'},
773                                   address2       => $data->{'address2'},
774                                   city           => $data->{'city'},
775                                   postcode       => $data->{'zipcode'},
776                                   country        => $data->{'country'},
777                                   email          => $notice_email,
778                                   itemcount      => $itemcount,
779                                   titles         => $titles,
780                                   outputformat   => defined $csvfilename ? 'csv' : defined $htmlfilename ? 'html' : defined $text_filename ? 'text' : '',
781                                 }
782                               );
783                         }
784                         unless ( $effective_mtt eq 'print' and $print_sent == 1 ) {
785                             # Just sent a print if not already done.
786                             C4::Letters::EnqueueLetter(
787                                 {   letter                 => $letter,
788                                     borrowernumber         => $borrowernumber,
789                                     message_transport_type => $effective_mtt,
790                                     from_address           => $admin_email_address,
791                                     to_address             => join(',', @emails_to_use),
792                                 }
793                             ) unless $test_mode;
794                             # A print notice should be sent only once per overdue level.
795                             # Without this check, a print could be sent twice or more if the library checks sms and email and print and the patron has no email or sms number.
796                             $print_sent = 1 if $effective_mtt eq 'print';
797                         }
798                     }
799                 }
800             }
801             $sth->finish;
802         }
803     }
804
805     if (@output_chunks) {
806         if ( defined $csvfilename ) {
807             print $csv_fh @output_chunks;        
808         }
809         elsif ( defined $htmlfilename ) {
810             print $fh @output_chunks;        
811         }
812         elsif ( defined $text_filename ) {
813             print $fh @output_chunks;        
814         }
815         elsif ($nomail){
816                 local $, = "\f";    # pagebreak
817                 print @output_chunks;
818         }
819         # Generate the content of the csv with headers
820         my $content;
821         if ( defined $csvfilename ) {
822             my $delimiter = C4::Context->preference('delimiter') || ';';
823             $content = join($delimiter, qw(title name surname address1 address2 zipcode city country email itemcount itemsinfo due_date issue_date)) . "\n";
824         }
825         else {
826             $content = "";
827         }
828         $content .= join( "\n", @output_chunks );
829
830         my $attachment = {
831             filename => defined $csvfilename ? 'attachment.csv' : 'attachment.txt',
832             type => 'text/plain',
833             content => $content, 
834         };
835
836         my $letter = {
837             title   => 'Overdue Notices',
838             content => 'These messages were not sent directly to the patrons.',
839         };
840
841         C4::Letters::EnqueueLetter(
842             {   letter                 => $letter,
843                 borrowernumber         => undef,
844                 message_transport_type => 'email',
845                 attachments            => [$attachment],
846                 to_address             => $branch_email_address,
847             }
848         ) unless $test_mode;
849     }
850
851 }
852 if ($csvfilename) {
853     # note that we're not testing on $csv_fh to prevent closing
854     # STDOUT.
855     close $csv_fh;
856 }
857
858 if ( defined $htmlfilename ) {
859   print $fh "</body>\n";
860   print $fh "</html>\n";
861   close $fh;
862 } elsif ( defined $text_filename ) {
863   close $fh;
864 }
865
866 =head1 INTERNAL METHODS
867
868 These methods are internal to the operation of overdue_notices.pl.
869
870 =head2 prepare_letter_for_printing
871
872 returns a string of text appropriate for printing in the event that an
873 overdue notice will not be sent to the patron's email
874 address. Depending on the desired output format, this may be a CSV
875 string, or a human-readable representation of the notice.
876
877 required parameters:
878   letter
879   borrowernumber
880
881 optional parameters:
882   outputformat
883
884 =cut
885
886 sub prepare_letter_for_printing {
887     my $params = shift;
888
889     return unless ref $params eq 'HASH';
890
891     foreach my $required_parameter (qw( letter borrowernumber )) {
892         return unless defined $params->{$required_parameter};
893     }
894
895     my $return;
896     chomp $params->{titles};
897     if ( exists $params->{'outputformat'} && $params->{'outputformat'} eq 'csv' ) {
898         if ($csv->combine(
899                 $params->{'firstname'}, $params->{'lastname'}, $params->{'address1'},  $params->{'address2'}, $params->{'postcode'},
900                 $params->{'city'}, $params->{'country'}, $params->{'email'}, $params->{'phone'}, $params->{'cardnumber'},
901                 $params->{'itemcount'}, $params->{'titles'}, $params->{'branchname'}, $params->{'letternumber'}
902             )
903           ) {
904             return $csv->string, "\n";
905         } else {
906             $verbose and warn 'combine failed on argument: ' . $csv->error_input;
907         }
908     } elsif ( exists $params->{'outputformat'} && $params->{'outputformat'} eq 'html' ) {
909       $return = "<pre>\n";
910       $return .= "$params->{'letter'}->{'content'}\n";
911       $return .= "\n</pre>\n";
912     } else {
913         $return .= "$params->{'letter'}->{'content'}\n";
914
915         # $return .= Data::Dumper->Dump( [ $params->{'borrowernumber'}, $params->{'letter'} ], [qw( borrowernumber letter )] );
916     }
917     return $return;
918 }
919