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