Bug 15157: Cronjob to debar Patrons with unpaid accountlines
[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 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) or patron home library (patron-homebranch).
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)  or patron home library (patron-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 or patron-homebranch only"
345     unless ( $frombranch eq 'item-issuebranch'
346     || $frombranch eq 'item-homebranch'
347     || $frombranch eq 'patron-homebranch' );
348 $frombranch =
349     C4::Context->preference('OverdueNoticeFrom') ne 'cron' ? C4::Context->preference('OverdueNoticeFrom') : $frombranch;
350 my $owning_library = ( $frombranch eq 'item-homebranch' ) ? 1 : 0;
351 my $patron_homelibrary = ( $frombranch eq 'patron-homebranch' ) ? 1 : 0;
352
353 my @overduebranches    = C4::Overdues::GetBranchcodesWithOverdueRules();    # Branches with overdue rules
354 my @branches;                                    # Branches passed as parameter with overdue rules
355 my $branchcount = scalar(@overduebranches);
356
357 my $overduebranch_word = scalar @overduebranches > 1 ? 'branches' : 'branch';
358 my $branchcodes_word = scalar @branchcodes > 1 ? 'branches' : 'branch';
359
360 my $PrintNoticesMaxLines = C4::Context->preference('PrintNoticesMaxLines');
361
362 if ($branchcount) {
363     $verbose and warn "Found $branchcount $overduebranch_word with first message enabled: " . join( ', ', map { "'$_'" } @overduebranches ), "\n";
364 } else {
365     die 'No branches with active overduerules';
366 }
367
368 if (@branchcodes) {
369     $verbose and warn "$branchcodes_word @branchcodes passed on parameter\n";
370     
371     # Getting libraries which have overdue rules
372     my %seen = map { $_ => 1 } @branchcodes;
373     @branches = grep { $seen{$_} } @overduebranches;
374     
375     
376     if (@branches) {
377
378         my $branch_word = scalar @branches > 1 ? 'branches' : 'branch';
379     $verbose and warn "$branch_word @branches have overdue rules\n";
380
381     } else {
382     
383         $verbose and warn "No active overduerules for $branchcodes_word  '@branchcodes'\n";
384         ( scalar grep { '' eq $_ } @branches )
385           or die "No active overduerules for DEFAULT either!";
386         $verbose and warn "Falling back on default rules for @branchcodes\n";
387         @branches = ('');
388     }
389 }
390 my $date_to_run;
391 my $date;
392 if ( $date_input ){
393     eval {
394         $date_to_run = dt_from_string( $date_input, 'iso' );
395     };
396     die "$date_input is not a valid date, aborting! Use a date in format YYYY-MM-DD."
397         if $@ or not $date_to_run;
398
399     # It's certainly useless to escape $date_input
400     # dt_from_string should not return something if $date_input is not correctly set.
401     $date = $dbh->quote( $date_input );
402 }
403 else {
404     $date="NOW()";
405     $date_to_run = dt_from_string();
406 }
407
408 # these are the fields that will be substituted into <<item.content>>
409 my @item_content_fields = split( /,/, $itemscontent );
410
411 binmode( STDOUT, ':encoding(UTF-8)' );
412
413
414 our $csv;       # the Text::CSV_XS object
415 our $csv_fh;    # the filehandle to the CSV file.
416 if ( defined $csvfilename ) {
417     my $sep_char = C4::Context->csv_delimiter;
418     $csv = Text::CSV_XS->new( { binary => 1 , sep_char => $sep_char } );
419     if ( $csvfilename eq '' ) {
420         $csv_fh = *STDOUT;
421     } else {
422         open $csv_fh, ">", $csvfilename or die "unable to open $csvfilename: $!";
423     }
424     if ( $csv->combine(qw(name surname address1 address2 zipcode city country email phone cardnumber itemcount itemsinfo branchname letternumber)) ) {
425         print $csv_fh $csv->string, "\n";
426     } else {
427         $verbose and warn 'combine failed on argument: ' . $csv->error_input;
428     }
429 }
430
431 @branches = @overduebranches unless @branches;
432 our $fh;
433 if ( defined $htmlfilename ) {
434   if ( $htmlfilename eq '' ) {
435     $fh = *STDOUT;
436   } else {
437     my $today = dt_from_string();
438     open $fh, ">:encoding(UTF-8)",File::Spec->catdir ($htmlfilename,"notices-".$today->ymd().".html");
439   }
440   
441   print $fh _get_html_start();
442 }
443 elsif ( defined $text_filename ) {
444   if ( $text_filename eq '' ) {
445     $fh = *STDOUT;
446   } else {
447     my $today = dt_from_string();
448     open $fh, ">:encoding(UTF-8)",File::Spec->catdir ($text_filename,"notices-".$today->ymd().".txt");
449   }
450 }
451
452 my %already_queued;
453 my %seen = map { $_ => 1 } @branches;
454 foreach my $branchcode (@branches) {
455     my $calendar;
456     if ( C4::Context->preference('OverdueNoticeCalendar') ) {
457         $calendar = Koha::Calendar->new( branchcode => $branchcode );
458         if ( $calendar->is_holiday($date_to_run) ) {
459             next;
460         }
461     }
462
463     my $library             = Koha::Libraries->find($branchcode);
464     my $admin_email_address = $library->from_email_address;
465     my $branch_email_address = C4::Context->preference('AddressForFailedOverdueNotices')
466       || $library->inbound_email_address;
467     my @output_chunks;    # may be sent to mail or stdout or csv file.
468
469     $verbose and print "======================================\n";
470     $verbose and warn sprintf "branchcode : '%s' using %s\n", $branchcode, $branch_email_address;
471
472     my $sql2 = <<"END_SQL";
473 SELECT biblio.*, items.*, issues.*, biblioitems.itemtype, branchname
474   FROM issues,items,biblio, biblioitems, branches b
475   WHERE items.itemnumber=issues.itemnumber
476     AND biblio.biblionumber   = items.biblionumber
477     AND b.branchcode = items.homebranch
478     AND biblio.biblionumber   = biblioitems.biblionumber
479     AND issues.borrowernumber = ?
480     AND items.itemlost = 0
481     AND TO_DAYS($date)-TO_DAYS(issues.date_due) >= 0
482 END_SQL
483
484     if ($owning_library) {
485         $sql2 .= ' AND items.homebranch = ? ' unless ($patron_homelibrary);
486     } else {
487         $sql2 .= ' AND issues.branchcode = ? ' unless ($patron_homelibrary);
488     }
489     my $sth2 = $dbh->prepare($sql2);
490
491     my $query = "SELECT * FROM overduerules WHERE delay1 IS NOT NULL AND branchcode = ? ";
492     $query .= " AND categorycode IN (".join( ',' , ('?') x @myborcat ).") " if (@myborcat);
493     $query .= " AND categorycode NOT IN (".join( ',' , ('?') x @myborcatout ).") " if (@myborcatout);
494     
495     my $rqoverduerules =  $dbh->prepare($query);
496     $rqoverduerules->execute($branchcode, @myborcat, @myborcatout);
497
498     # We get default rules if there is no rule for this branch
499     if($rqoverduerules->rows == 0){
500         $query = "SELECT * FROM overduerules WHERE delay1 IS NOT NULL AND branchcode = '' ";
501         $query .= " AND categorycode IN (".join( ',' , ('?') x @myborcat ).") " if (@myborcat);
502         $query .= " AND categorycode NOT IN (".join( ',' , ('?') x @myborcatout ).") " if (@myborcatout);
503         
504         $rqoverduerules = $dbh->prepare($query);
505         $rqoverduerules->execute(@myborcat, @myborcatout);
506     }
507
508     # my $outfile = 'overdues_' . ( $mybranch || $branchcode || 'default' );
509     while ( my $overdue_rules = $rqoverduerules->fetchrow_hashref ) {
510       PERIOD: foreach my $i ( 1 .. 3 ) {
511
512             $verbose and warn "branch '$branchcode', categorycode = $overdue_rules->{categorycode} pass $i\n";
513
514             my $mindays = $overdue_rules->{"delay$i"};    # the notice will be sent after mindays days (grace period)
515             my $maxdays = (
516                   $overdue_rules->{ "delay" . ( $i + 1 ) }
517                 ? $overdue_rules->{ "delay" . ( $i + 1 ) } - 1
518                 : ($MAX)
519             );                                            # issues being more than maxdays late are managed somewhere else. (borrower probably suspended)
520
521             next unless defined $mindays;
522
523             if ( !$overdue_rules->{"letter$i"} ) {
524                 $verbose and warn sprintf "No letter code found for pass %s\n", $i;
525                 next PERIOD;
526             }
527             $verbose and warn sprintf "Using letter code '%s' for pass %s\n", $overdue_rules->{"letter$i"}, $i;
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             if ( $verbose > 1 ){
564                 warn sprintf "--------Borrower SQL------\n";
565                 warn $borrower_sql . "\n $branchcode | " . $overdue_rules->{'categorycode'} . "\n ($mindays, $maxdays, ".  $date_to_run->datetime() .")\n";
566                 warn sprintf "--------------------------\n";
567             }
568             $verbose and warn sprintf "Found %s borrowers with overdues\n", $sth->rows;
569             my $borrowernumber;
570             while ( my $data = $sth->fetchrow_hashref ) {
571
572                 # check the borrower has at least one item that matches
573                 my $days_between;
574                 if ( C4::Context->preference('OverdueNoticeCalendar') )
575                 {
576                     $days_between =
577                       $calendar->days_between( dt_from_string($data->{date_due}),
578                         $date_to_run );
579                 }
580                 else {
581                     $days_between =
582                       $date_to_run->delta_days( dt_from_string($data->{date_due}) );
583                 }
584                 $days_between = $days_between->in_units('days');
585                 if ($triggered) {
586                     if ( $mindays != $days_between ) {
587                         next;
588                     }
589                 }
590                 else {
591                     unless (   $days_between >= $mindays
592                         && $days_between <= $maxdays )
593                     {
594                         next;
595                     }
596                 }
597                 if (defined $borrowernumber && $borrowernumber eq $data->{'borrowernumber'}){
598 # we have already dealt with this borrower
599                     $verbose and warn "already dealt with this borrower $borrowernumber";
600                     next;
601                 }
602                 $borrowernumber = $data->{'borrowernumber'};
603                 next if ( $patron_homelibrary && $already_queued{"$borrowernumber$i"} );
604                 my $borr = sprintf( "%s%s%s (%s)",
605                     $data->{'surname'} || '',
606                     $data->{'firstname'} && $data->{'surname'} ? ', ' : '',
607                     $data->{'firstname'} || '',
608                     $borrowernumber );
609                 $verbose and warn "borrower $borr has items triggering level $i.\n";
610
611                 my $patron = Koha::Patrons->find( $borrowernumber );
612                 if ($patron_homelibrary) {
613                     $branchcode           = $patron->branchcode;
614                     $library              = Koha::Libraries->find($branchcode);
615                     $admin_email_address  = $library->from_email_address;
616                     $branch_email_address = C4::Context->preference('AddressForFailedOverdueNotices')
617                         || $library->inbound_email_address;
618                 }
619                 @emails_to_use = ();
620                 my $notice_email = $patron->notice_email_address;
621                 unless ($nomail) {
622                     if (@emails) {
623                         foreach (@emails) {
624                             push @emails_to_use, $data->{$_} if ( $data->{$_} );
625                         }
626                     }
627                     else {
628                         push @emails_to_use, $notice_email if ($notice_email);
629                     }
630                 }
631
632                 my $letter = Koha::Notice::Templates->find_effective_template(
633                     {
634                         module     => 'circulation',
635                         code       => $overdue_rules->{"letter$i"},
636                         branchcode => $branchcode,
637                         lang       => $patron->lang
638                     }
639                 );
640
641                 unless ($letter) {
642                     $verbose and warn qq|Message '$overdue_rules->{"letter$i"}' content not found|;
643
644                     # might as well skip while PERIOD, no other borrowers are going to work.
645                     # FIXME : Does this mean a letter must be defined in order to trigger a debar ?
646                     next PERIOD;
647                 }
648     
649                 if ( $overdue_rules->{"debarred$i"} ) {
650     
651                     #action taken is debarring
652                     AddUniqueDebarment(
653                         {
654                             borrowernumber => $borrowernumber,
655                             type           => 'OVERDUES',
656                             comment => "OVERDUES_PROCESS " .  output_pref( dt_from_string() ),
657                         }
658                     ) unless $test_mode;
659                     $verbose and warn "debarring $borr\n";
660                 }
661                 my @params = $patron_homelibrary ? ($borrowernumber) : ( $borrowernumber, $branchcode );
662
663                 $sth2->execute(@params);
664                 my $itemcount = 0;
665                 my $titles = "";
666                 my @items = ();
667                 
668                 my $j = 0;
669                 my $exceededPrintNoticesMaxLines = 0;
670                 while ( my $item_info = $sth2->fetchrow_hashref() ) {
671                     if ( C4::Context->preference('OverdueNoticeCalendar') ) {
672                         $days_between =
673                           $calendar->days_between(
674                             dt_from_string( $item_info->{date_due} ), $date_to_run );
675                     }
676                     else {
677                         $days_between =
678                           $date_to_run->delta_days(
679                             dt_from_string( $item_info->{date_due} ) );
680                     }
681                     $days_between = $days_between->in_units('days');
682                     if ($listall){
683                         unless ($days_between >= 1 and $days_between <= $MAX){
684                             next;
685                         }
686                     }
687                     else {
688                         if ($triggered) {
689                             if ( $mindays != $days_between ) {
690                                 next;
691                             }
692                         }
693                         else {
694                             unless ( $days_between >= $mindays
695                                 && $days_between <= $maxdays )
696                             {
697                                 next;
698                             }
699                         }
700                     }
701
702                     if ( ( scalar(@emails_to_use) == 0 || $nomail ) && $PrintNoticesMaxLines && $j >= $PrintNoticesMaxLines ) {
703                       $exceededPrintNoticesMaxLines = 1;
704                       last;
705                     }
706                     next if $patron_homelibrary and !grep { $seen{ $item_info->{branchcode} } } @branches;
707                     $j++;
708
709                     $titles .= C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields, dateonly => 1 } );
710                     $itemcount++;
711                     push @items, $item_info;
712                 }
713                 $sth2->finish;
714
715                 my @message_transport_types = @{ GetOverdueMessageTransportTypes( $branchcode, $overdue_rules->{categorycode}, $i) };
716                 @message_transport_types = @{ GetOverdueMessageTransportTypes( q{}, $overdue_rules->{categorycode}, $i) }
717                     unless @message_transport_types;
718
719
720                 my $print_sent = 0; # A print notice is not yet sent for this patron
721                 for my $mtt ( @message_transport_types ) {
722                     next if $mtt eq 'itiva';
723                     my $effective_mtt = $mtt;
724                     if ( ($mtt eq 'email' and not scalar @emails_to_use) or ($mtt eq 'sms' and not $data->{smsalertnumber}) ) {
725                         # email or sms is requested but not exist, do a print.
726                         $effective_mtt = 'print';
727                     }
728                     splice @items, $PrintNoticesMaxLines if $effective_mtt eq 'print' && $PrintNoticesMaxLines && scalar @items > $PrintNoticesMaxLines;
729                     #catch the case where we are sending a print to someone with an email
730
731                     my $letter_exists = Koha::Notice::Templates->find_effective_template(
732                         {
733                             module     => 'circulation',
734                             code       => $overdue_rules->{"letter$i"},
735                             message_transport_type => $effective_mtt,
736                             branchcode => $branchcode,
737                             lang       => $patron->lang
738                         }
739                     );
740
741                     my $letter = parse_overdues_letter(
742                         {   letter_code     => $overdue_rules->{"letter$i"},
743                             borrowernumber  => $borrowernumber,
744                             branchcode      => $branchcode,
745                             items           => \@items,
746                             substitute      => {    # this appears to be a hack to overcome incomplete features in this code.
747                                                 bib             => $library->branchname, # maybe 'bib' is a typo for 'lib<rary>'?
748                                                 'items.content' => $titles,
749                                                 'count'         => $itemcount,
750                                                },
751                             # If there is no template defined for the requested letter
752                             # Fallback on the original type
753                             message_transport_type => $letter_exists ? $effective_mtt : $mtt,
754                         }
755                     );
756                     unless ($letter && $letter->{content}) {
757                         $verbose and warn qq|Message '$overdue_rules->{"letter$i"}' content not found|;
758                         # this transport doesn't have a configured notice, so try another
759                         next;
760                     }
761
762                     if ( $exceededPrintNoticesMaxLines ) {
763                       $letter->{'content'} .= "List too long for form; please check your account online for a complete list of your overdue items.";
764                     }
765
766                     my @misses = grep { /./ } map { /^([^>]*)[>]+/; ( $1 || '' ); } split /\</, $letter->{'content'};
767                     if (@misses) {
768                         $verbose and warn "The following terms were not matched and replaced: \n\t" . join "\n\t", @misses;
769                     }
770
771                     if ($nomail) {
772                         push @output_chunks,
773                           prepare_letter_for_printing(
774                           {   letter         => $letter,
775                               borrowernumber => $borrowernumber,
776                               firstname      => $data->{'firstname'},
777                               lastname       => $data->{'surname'},
778                               address1       => $data->{'address'},
779                               address2       => $data->{'address2'},
780                               city           => $data->{'city'},
781                               phone          => $data->{'phone'},
782                               cardnumber     => $data->{'cardnumber'},
783                               branchname     => $library->branchname,
784                               letternumber   => $i,
785                               postcode       => $data->{'zipcode'},
786                               country        => $data->{'country'},
787                               email          => $notice_email,
788                               itemcount      => $itemcount,
789                               titles         => $titles,
790                               outputformat   => defined $csvfilename ? 'csv' : defined $htmlfilename ? 'html' : defined $text_filename ? 'text' : '',
791                             }
792                           );
793                     } else {
794                         if ( ($mtt eq 'email' and not scalar @emails_to_use) or ($mtt eq 'sms' and not $data->{smsalertnumber}) ) {
795                             push @output_chunks,
796                               prepare_letter_for_printing(
797                               {   letter         => $letter,
798                                   borrowernumber => $borrowernumber,
799                                   firstname      => $data->{'firstname'},
800                                   lastname       => $data->{'surname'},
801                                   address1       => $data->{'address'},
802                                   address2       => $data->{'address2'},
803                                   city           => $data->{'city'},
804                                   postcode       => $data->{'zipcode'},
805                                   country        => $data->{'country'},
806                                   email          => $notice_email,
807                                   itemcount      => $itemcount,
808                                   titles         => $titles,
809                                   outputformat   => defined $csvfilename ? 'csv' : defined $htmlfilename ? 'html' : defined $text_filename ? 'text' : '',
810                                 }
811                               );
812                         }
813                         unless ( $effective_mtt eq 'print' and $print_sent == 1 ) {
814                             # Just sent a print if not already done.
815                             C4::Letters::EnqueueLetter(
816                                 {   letter                 => $letter,
817                                     borrowernumber         => $borrowernumber,
818                                     message_transport_type => $effective_mtt,
819                                     from_address           => $admin_email_address,
820                                     to_address             => join(',', @emails_to_use),
821                                     reply_address          => $library->inbound_email_address,
822                                 }
823                             ) unless $test_mode;
824                             # A print notice should be sent only once per overdue level.
825                             # 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.
826                             $print_sent = 1 if $effective_mtt eq 'print';
827                         }
828                     }
829                 }
830                 $already_queued{"$borrowernumber$i"} = 1;
831             }
832             $sth->finish;
833         }
834     }
835
836     if (@output_chunks) {
837         if ( defined $csvfilename ) {
838             print $csv_fh @output_chunks;        
839         }
840         elsif ( defined $htmlfilename ) {
841             print $fh @output_chunks;        
842         }
843         elsif ( defined $text_filename ) {
844             print $fh @output_chunks;        
845         }
846         elsif ($nomail){
847                 local $, = "\f";    # pagebreak
848                 print @output_chunks;
849         }
850         # Generate the content of the csv with headers
851         my $content;
852         if ( defined $csvfilename ) {
853             my $delimiter = C4::Context->csv_delimiter;
854             $content = join($delimiter, qw(title name surname address1 address2 zipcode city country email itemcount itemsinfo due_date issue_date)) . "\n";
855             $content .= join( "\n", @output_chunks );
856         } elsif ( defined $htmlfilename ) {
857             $content = _get_html_start();
858             $content .= join( "\n", @output_chunks );
859             $content .= _get_html_end();
860         } else {
861             $content = join( "\n", @output_chunks );
862         }
863
864         if ( C4::Context->preference('EmailOverduesNoEmail') ) {
865             my $attachment = {
866                 filename => defined $csvfilename ? 'attachment.csv' : defined $htmlfilename ? 'attachment.html' : 'attachment.txt',
867                 type => defined $htmlfilename ? 'text/html' : 'text/plain',
868                 content => $content,
869             };
870
871             my $letter = {
872                 title   => 'Overdue Notices',
873                 content => 'These messages were not sent directly to the patrons.',
874             };
875
876             C4::Letters::EnqueueLetter(
877                 {   letter                 => $letter,
878                     borrowernumber         => undef,
879                     message_transport_type => 'email',
880                     attachments            => [$attachment],
881                     to_address             => $branch_email_address,
882                 }
883             ) unless $test_mode;
884         }
885     }
886
887 }
888 if ($csvfilename) {
889     # note that we're not testing on $csv_fh to prevent closing
890     # STDOUT.
891     close $csv_fh;
892 }
893
894 if ( defined $htmlfilename ) {
895   print $fh _get_html_end();
896   close $fh;
897 } elsif ( defined $text_filename ) {
898   close $fh;
899 }
900
901 =head1 INTERNAL METHODS
902
903 These methods are internal to the operation of overdue_notices.pl.
904
905 =head2 prepare_letter_for_printing
906
907 returns a string of text appropriate for printing in the event that an
908 overdue notice will not be sent to the patron's email
909 address. Depending on the desired output format, this may be a CSV
910 string, or a human-readable representation of the notice.
911
912 required parameters:
913   letter
914   borrowernumber
915
916 optional parameters:
917   outputformat
918
919 =cut
920
921 sub prepare_letter_for_printing {
922     my $params = shift;
923
924     return unless ref $params eq 'HASH';
925
926     foreach my $required_parameter (qw( letter borrowernumber )) {
927         return unless defined $params->{$required_parameter};
928     }
929
930     my $return;
931     chomp $params->{titles};
932     if ( exists $params->{'outputformat'} && $params->{'outputformat'} eq 'csv' ) {
933         if ($csv->combine(
934                 $params->{'firstname'}, $params->{'lastname'}, $params->{'address1'},  $params->{'address2'}, $params->{'postcode'},
935                 $params->{'city'}, $params->{'country'}, $params->{'email'}, $params->{'phone'}, $params->{'cardnumber'},
936                 $params->{'itemcount'}, $params->{'titles'}, $params->{'branchname'}, $params->{'letternumber'}
937             )
938           ) {
939             return $csv->string, "\n";
940         } else {
941             $verbose and warn 'combine failed on argument: ' . $csv->error_input;
942         }
943     } elsif ( exists $params->{'outputformat'} && $params->{'outputformat'} eq 'html' ) {
944       $return = "<pre>\n";
945       $return .= "$params->{'letter'}->{'content'}\n";
946       $return .= "\n</pre>\n";
947     } else {
948         $return .= "$params->{'letter'}->{'content'}\n";
949
950         # $return .= Data::Dumper->Dump( [ $params->{'borrowernumber'}, $params->{'letter'} ], [qw( borrowernumber letter )] );
951     }
952     return $return;
953 }
954
955 =head2 _get_html_start
956
957 Return the start of a HTML document, including html, head and the start body
958 tags. This should be usable both in the HTML file written to disc, and in the
959 attachment.html sent as email.
960
961 =cut
962
963 sub _get_html_start {
964
965     return "<html>
966 <head>
967 <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />
968 <style type='text/css'>
969 pre {page-break-after: always;}
970 pre {white-space: pre-wrap;}
971 pre {white-space: -moz-pre-wrap;}
972 pre {white-space: -o-pre-wrap;}
973 pre {word-wrap: break-work;}
974 </style>
975 </head>
976 <body>";
977
978 }
979
980 =head2 _get_html_end
981
982 Return the end of an HTML document, namely the closing body and html tags.
983
984 =cut
985
986 sub _get_html_end {
987
988     return "</body>
989 </html>";
990
991 }
992
993 cronlogaction({ action => 'End', info => "COMPLETED" });