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