From 7b6f0d6f19375837e06e53543661b1d5ed55a9ef Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Mon, 19 Aug 2024 17:21:09 +0100 Subject: [PATCH] Bug 10190: Rework logic of overdue_notices The original logic for digesting overdues in the overdue notices script was flawed given the additional itemtype handling introduced in this patchset and the way in which it handles effective fallbacks for rules. This patch tidies the file and reworks the logic so that we fetch overdues by patron category and branch and then iterate through those comparing to the correct effective rules and combing them into digest groups based on notice + trigger level. --- misc/cronjobs/overdue_notices.pl | 917 +++++++++++++++++-------------- 1 file changed, 505 insertions(+), 412 deletions(-) diff --git a/misc/cronjobs/overdue_notices.pl b/misc/cronjobs/overdue_notices.pl index 204bef008f3..6452f35f7e1 100755 --- a/misc/cronjobs/overdue_notices.pl +++ b/misc/cronjobs/overdue_notices.pl @@ -29,10 +29,10 @@ use DateTime::Duration; use Koha::Script -cron; use C4::Context; use C4::Letters; -use C4::Overdues qw( parse_overdues_letter ); -use C4::Log qw( cronlogaction ); +use C4::Overdues qw( parse_overdues_letter ); +use C4::Log qw( cronlogaction ); use Koha::Patron::Debarments qw( AddUniqueDebarment ); -use Koha::DateUtils qw( dt_from_string output_pref ); +use Koha::DateUtils qw( dt_from_string output_pref ); use Koha::Calendar; use Koha::Libraries; use Koha::Acquisition::Currencies; @@ -298,22 +298,23 @@ alert them of items that have just become due. # These variables are set by command line options. # They are initially set to default values. -my $dbh = C4::Context->dbh(); -my $help = 0; -my $man = 0; -my $verbose = 0; -my $nomail = 0; -my $MAX = 90; -my $test_mode = 0; -my $frombranch = 'item-issuebranch'; -my @branchcodes; # Branch(es) passed as parameter +my $dbh = C4::Context->dbh(); +my $help = 0; +my $man = 0; +my $verbose = 0; +my $nomail = 0; +my $MAX = 90; +my $test_mode = 0; +my $frombranch = 'item-issuebranch'; +my $itype_level = C4::Context->preference('item-level_itypes') ? 'item' : 'biblioitem'; +my @branchcodes; # Branch(es) passed as parameter my @emails_to_use; # Emails to use for messaging my @emails; # Emails given in command-line parameters my $csvfilename; my $htmlfilename; my $text_filename; -my $triggered = 0; -my $listall = 0; +my $triggered = 0; +my $listall = 0; my $itemscontent = join( ',', qw( date_due title barcode author itemnumber ) ); my @myitemtypes; my @myitemtypesout; @@ -321,7 +322,7 @@ my @myborcat; my @myborcatout; my ( $date_input, $today ); -my $command_line_options = join(" ",@ARGV); +my $command_line_options = join( " ", @ARGV ); GetOptions( 'help|?' => \$help, @@ -359,20 +360,22 @@ die "--frombranch takes item-homebranch or item-issuebranch or patron-homebranch || $frombranch eq 'patron-homebranch' ); $frombranch = C4::Context->preference('OverdueNoticeFrom') ne 'cron' ? C4::Context->preference('OverdueNoticeFrom') : $frombranch; -my $owning_library = ( $frombranch eq 'item-homebranch' ) ? 1 : 0; +my $owning_library = ( $frombranch eq 'item-homebranch' ) ? 1 : 0; my $patron_homelibrary = ( $frombranch eq 'patron-homebranch' ) ? 1 : 0; -my @overduebranches = C4::Overdues::GetBranchcodesWithOverdueRules(); # Branches with overdue rules -my @branches; # Branches passed as parameter with overdue rules +my @overduebranches = C4::Overdues::GetBranchcodesWithOverdueRules(); # Branches with overdue rules +my @branches; # Branches passed as parameter with overdue rules my $branchcount = scalar(@overduebranches); my $overduebranch_word = scalar @overduebranches > 1 ? 'branches' : 'branch'; -my $branchcodes_word = scalar @branchcodes > 1 ? 'branches' : 'branch'; +my $branchcodes_word = scalar @branchcodes > 1 ? 'branches' : 'branch'; my $PrintNoticesMaxLines = C4::Context->preference('PrintNoticesMaxLines'); if ($branchcount) { - $verbose and warn "Found $branchcount $overduebranch_word with first message enabled: " . join( ', ', map { "'$_'" } @overduebranches ), "\n"; + $verbose + and warn "Found $branchcount $overduebranch_word with first message enabled: " + . join( ', ', map { "'$_'" } @overduebranches ), "\n"; } else { die 'No branches with active overduerules'; } @@ -384,36 +387,32 @@ if (@branchcodes) { my %seen = map { $_ => 1 } @branchcodes; @branches = grep { $seen{$_} } @overduebranches; - if (@branches) { my $branch_word = scalar @branches > 1 ? 'branches' : 'branch'; - $verbose and warn "$branch_word @branches have overdue rules\n"; + $verbose and warn "$branch_word @branches have overdue rules\n"; } else { $verbose and warn "No active overduerules for $branchcodes_word '@branchcodes'\n"; ( scalar grep { '' eq $_ } @branches ) - or die "No active overduerules for DEFAULT either!"; + or die "No active overduerules for DEFAULT either!"; $verbose and warn "Falling back on default rules for @branchcodes\n"; @branches = (''); } } my $date_to_run; my $date; -if ( $date_input ){ - eval { - $date_to_run = dt_from_string( $date_input, 'iso' ); - }; +if ($date_input) { + eval { $date_to_run = dt_from_string( $date_input, 'iso' ); }; die "$date_input is not a valid date, aborting! Use a date in format YYYY-MM-DD." if $@ or not $date_to_run; # It's certainly useless to escape $date_input # dt_from_string should not return something if $date_input is not correctly set. - $date = $dbh->quote( $date_input ); -} -else { - $date="NOW()"; + $date = $dbh->quote($date_input); +} else { + $date = "NOW()"; $date_to_run = dt_from_string(); } @@ -422,18 +421,22 @@ my @item_content_fields = split( /,/, $itemscontent ); binmode( STDOUT, ':encoding(UTF-8)' ); - our $csv; # the Text::CSV_XS object our $csv_fh; # the filehandle to the CSV file. if ( defined $csvfilename ) { my $sep_char = C4::Context->csv_delimiter; - $csv = Text::CSV_XS->new( { binary => 1 , sep_char => $sep_char } ); + $csv = Text::CSV_XS->new( { binary => 1, sep_char => $sep_char } ); if ( $csvfilename eq '' ) { $csv_fh = *STDOUT; } else { open $csv_fh, ">", $csvfilename or die "unable to open $csvfilename: $!"; } - if ( $csv->combine(qw(name surname address1 address2 zipcode city country email phone cardnumber itemcount itemsinfo branchname letternumber)) ) { + if ( + $csv->combine( + qw(name surname address1 address2 zipcode city country email phone cardnumber itemcount itemsinfo branchname letternumber) + ) + ) + { print $csv_fh $csv->string, "\n"; } else { $verbose and warn 'combine failed on argument: ' . $csv->error_input; @@ -441,30 +444,54 @@ if ( defined $csvfilename ) { } @branches = @overduebranches unless @branches; + +# Setup output file if requested our $fh; if ( defined $htmlfilename ) { - if ( $htmlfilename eq '' ) { - $fh = *STDOUT; - } else { - my $today = dt_from_string(); - open $fh, ">:encoding(UTF-8)",File::Spec->catdir ($htmlfilename,"notices-".$today->ymd().".html"); - } - - print $fh _get_html_start(); + if ( $htmlfilename eq '' ) { + $fh = *STDOUT; + } else { + my $today = dt_from_string(); + open $fh, ">:encoding(UTF-8)", File::Spec->catdir( $htmlfilename, "notices-" . $today->ymd() . ".html" ); + } + + print $fh _get_html_start(); +} elsif ( defined $text_filename ) { + if ( $text_filename eq '' ) { + $fh = *STDOUT; + } else { + my $today = dt_from_string(); + open $fh, ">:encoding(UTF-8)", File::Spec->catdir( $text_filename, "notices-" . $today->ymd() . ".txt" ); + } +} + +# Setup category list +my @categories; +if (@myborcat) { + @categories = @myborcat; +} elsif (@myborcatout) { + @categories = Koha::Patron::Categories->search( { catagorycode => { 'not_in' => \@myborcatout } } ) + ->get_column('categorycode'); +} else { + @categories = Koha::Patron::Categories->search()->get_column('categorycode'); } -elsif ( defined $text_filename ) { - if ( $text_filename eq '' ) { - $fh = *STDOUT; - } else { - my $today = dt_from_string(); - open $fh, ">:encoding(UTF-8)",File::Spec->catdir ($text_filename,"notices-".$today->ymd().".txt"); - } + +# Setup itemtype list +my @itemtypes; +if (@myitemtypes) { + @itemtypes = @myitemtypes; +} elsif (@myitemtypesout) { + @itemtypes = + Koha::ItemTypes->search( { itemtype => { 'not_in' => \@myitemtypesout } } )->get_column('itemtype'); +} else { + @itemtypes = Koha::ItemTypes->search()->get_column('itemtype'); } my %already_queued; my %seen = map { $_ => 1 } @branches; # Work through branches +my @output_chunks; foreach my $branchcode (@branches) { my $calendar; if ( C4::Context->preference('OverdueNoticeCalendar') ) { @@ -474,62 +501,132 @@ foreach my $branchcode (@branches) { } } - my $library = Koha::Libraries->find($branchcode); - my $admin_email_address = $library->from_email_address; + my $library = Koha::Libraries->find($branchcode); + my $admin_email_address = $library->from_email_address; my $branch_email_address = C4::Context->preference('AddressForFailedOverdueNotices') - || $library->inbound_email_address; - my @output_chunks; # may be sent to mail or stdout or csv file. + || $library->inbound_email_address; + @output_chunks = (); # may be sent to mail or stdout or csv file. $verbose and print "======================================\n"; $verbose and warn sprintf "branchcode : '%s' using %s\n", $branchcode, $branch_email_address; - my $sql2 = <<"END_SQL"; -SELECT biblio.*, items.*, issues.*, biblioitems.itemtype, branchname - FROM issues,items,biblio, biblioitems, branches b - WHERE items.itemnumber=issues.itemnumber - AND biblio.biblionumber = items.biblionumber - AND b.branchcode = items.homebranch - AND biblio.biblionumber = biblioitems.biblionumber - AND issues.borrowernumber = ? - AND items.itemlost = 0 + # Work through patron categories + for my $category (@categories) { + + # Fetch all overdues for patron categories that want overdues and where the item is not lost and group by borrower + my $borrower_sql = <<"END_SQL"; +SELECT + issues.borrowernumber, + borrowers.firstname, + borrowers.surname, + borrowers.address, + borrowers.address2, + borrowers.city, + borrowers.zipcode, + borrowers.country, + borrowers.email, + borrowers.emailpro, + borrowers.B_email, + borrowers.smsalertnumber, + borrowers.phone, + borrowers.cardnumber, + biblio.*, + biblioitems.itemtype AS b_itemtype, + items.*, + issues.*, + branches.branchname +FROM + issues +JOIN + borrowers ON issues.borrowernumber = borrowers.borrowernumber +JOIN + categories ON borrowers.categorycode = categories.categorycode +JOIN + items ON issues.itemnumber = items.itemnumber +JOIN + biblio ON biblio.biblionumber = items.biblionumber +JOIN + biblioitems ON biblio.biblionumber = biblioitems.biblionumber +JOIN + branches ON branches.branchcode = items.homebranch +WHERE + items.itemlost = 0 AND TO_DAYS($date)-TO_DAYS(issues.date_due) >= 0 END_SQL - if ($owning_library) { - $sql2 .= ' AND items.homebranch = ? ' unless ($patron_homelibrary); - } else { - $sql2 .= ' AND issues.branchcode = ? ' unless ($patron_homelibrary); - } - my $sth2 = $dbh->prepare($sql2); - - my @categories; - if (@myborcat) { - @categories = @myborcat; - } elsif (@myborcatout) { - @categories = Koha::Patron::Categories->search( { catagorycode => { 'not_in' => \@myborcatout } } ) - ->get_column('categorycode'); - } else { - @categories = Koha::Patron::Categories->search()->get_column('categorycode'); - } + my @borrower_parameters; + if ($branchcode) { + if ($owning_library) { + $borrower_sql .= ' AND items.homebranch=? '; + } else { + $borrower_sql .= ' AND issues.branchcode=? '; + } + push @borrower_parameters, $branchcode; + } + if ($category) { + $borrower_sql .= ' AND borrowers.categorycode=? '; + push @borrower_parameters, $category; + } + if (@itemtypes) { + my $placeholders = join( ", ", ("?") x @itemtypes ); + if ( $itype_level eq 'item' ) { + $borrower_sql .= " AND items.itype IN ($placeholders) "; + } else { + $borrower_sql .= " AND biblioitems.itemtype IN ($placeholders) "; + } + push @borrower_parameters, @itemtypes; + } + $borrower_sql .= ' AND categories.overduenoticerequired=1 ORDER BY issues.borrowernumber'; + + # $sth gets borrower info if at least one overdue item has triggered the overdue action. + my $sth = $dbh->prepare($borrower_sql); + $sth->execute(@borrower_parameters); + + if ( $verbose > 1 ) { + warn sprintf "--------Borrower SQL------\n"; + warn $borrower_sql + . "\n $branchcode | " + . "'$category' | " + . join( "','", @itemtypes ) . " | " + . $date_to_run->datetime() . ")\n"; + warn sprintf "--------------------------\n"; + } + $verbose and warn sprintf "Found %s overdues for $category on $date_to_run\n", $sth->rows; - my @itemtypes; - if (@myitemtypes) { - @itemtypes = @myitemtypes; - } elsif (@myitemtypesout) { - @itemtypes = - Koha::ItemTypes->search( { itemtype => { 'not_in' => \@myitemtypesout } } )->get_column('itemtype'); - } else { - @itemtypes = Koha::ItemTypes->search()->get_column('itemtype'); - } + my $borrowernumber; + my $borrower_overdues = {}; - # Work through category and itemtype combinations to catch overdue rules for each - for my $category (@categories) { - for my $itemtype (@itemtypes) { + # Iterate over all overdues + OVERDUE: while ( my $data = $sth->fetchrow_hashref ) { + my $itemtype = $data->{itype} // $data->{b_itemtype}; + + # Collect triggers or act on them if we're switching borrower + if ( !defined $borrower_overdues->{borrowernumber} ) { + if ($verbose) { + warn "\n-----------------------------------------\n"; + warn "Collecting overdue triggers for borrower " . $data->{borrowernumber} . "\n"; + } + $borrower_overdues = { borrowernumber => $data->{borrowernumber}, branchcode => $branchcode }; + } elsif ( $borrower_overdues->{borrowernumber} ne $data->{borrowernumber} ) { + $verbose and warn "Collected overdue triggers for " . $borrower_overdues->{borrowernumber} . "\n"; + _enact_trigger($borrower_overdues); + if ($verbose) { + warn "\n-----------------------------------------\n"; + warn "Collecting overdue triggers for borrower " . $data->{borrowernumber} . "\n"; + } + $borrower_overdues = { borrowernumber => $data->{borrowernumber}, branchcode => $branchcode }; + } + + $verbose + and warn "\nProcessing overdue " + . $data->{issue_id} + . " with branch = '$branchcode', categorycode = '$category' and itemtype = '$itemtype'\n"; + + # Work through triggers until we run out of rules or find a match my $i = 0; - # Work through triggers until we run out of rules - PERIOD: while (1) { + PERIOD: while (1) { $i++; - my $ii = $i + 1; + my $ii = $i + 1; my $overdue_rules = Koha::CirculationRules->get_effective_rules( { rules => [ @@ -541,361 +638,123 @@ END_SQL itemtype => $itemtype, } ); - $verbose and warn "branch '$branchcode', categorycode = $category, itemtype = $itemtype, pass $i\n"; if ( !defined( $overdue_rules->{ "overdue_$i" . '_delay' } ) ) { last PERIOD; } my $mindays = - $overdue_rules->{ "overdue_$i" . '_delay' }; # the notice will be sent after mindays days (grace period) + $overdue_rules->{ "overdue_$i" . '_delay' } + ; # the notice will be sent after mindays days (grace period) my $maxdays = ( $overdue_rules->{ "overdue_$ii" . '_delay' } ? $overdue_rules->{ "overdue_$ii" . '_delay' } - 1 : ($MAX) - ); # issues being more than maxdays late are managed somewhere else. (borrower probably suspended) - - next unless defined $mindays; + ); # issues being more than maxdays late are managed somewhere else. (borrower probably suspended) - if ( !$overdue_rules->{"overdue_$i".'_notice'} ) { - $verbose and warn sprintf "No letter code found for pass %s\n", $i; + if ( !$overdue_rules->{ "overdue_$i" . '_notice' } ) { + $verbose and warn sprintf "Trigger %s skipped, No letter code found\n", $i; next PERIOD; } - $verbose and warn sprintf "Using letter code '%s' for pass %s\n", $overdue_rules->{"overdue_$i".'_notice'}, $i; # $letter->{'content'} is the text of the mail that is sent. # this text contains fields that are replaced by their value. Those fields must be written between brackets # The following fields are available : - # 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). + # 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). # - # Fetch all overdue issues where the item is not lost and the borrower category wants overdue notices grouping by borrower - my $borrower_sql = <<"END_SQL"; -SELECT issues.borrowernumber, firstname, surname, address, address2, city, zipcode, country, email, emailpro, B_email, smsalertnumber, phone, cardnumber, date_due -FROM issues,borrowers,categories,items -WHERE issues.borrowernumber=borrowers.borrowernumber -AND borrowers.categorycode=categories.categorycode -AND issues.itemnumber = items.itemnumber -AND items.itemlost = 0 -AND TO_DAYS($date)-TO_DAYS(issues.date_due) >= 0 -END_SQL - my @borrower_parameters; - if ($branchcode) { - if($owning_library) { - $borrower_sql .= ' AND items.homebranch=? '; - } else { - $borrower_sql .= ' AND issues.branchcode=? '; - } - push @borrower_parameters, $branchcode; - } - if ( $category ) { - $borrower_sql .= ' AND borrowers.categorycode=? '; - push @borrower_parameters, $category; - } - $borrower_sql .= ' AND categories.overduenoticerequired=1 ORDER BY issues.borrowernumber'; - - # $sth gets borrower info if at least one overdue item has triggered the overdue action. - my $sth = $dbh->prepare($borrower_sql); - $sth->execute(@borrower_parameters); - - if ( $verbose > 1 ){ - warn sprintf "--------Borrower SQL------\n"; - warn $borrower_sql . "\n $branchcode | " . $category . "\n ($mindays, $maxdays, ". $date_to_run->datetime() .")\n"; - warn sprintf "--------------------------\n"; + # Check the overdue period matches + my $days_between; + if ( C4::Context->preference('OverdueNoticeCalendar') ) { + $days_between = $calendar->days_between( + dt_from_string( $data->{date_due} ), + $date_to_run + ); + } else { + $days_between = + $date_to_run->delta_days( dt_from_string( $data->{date_due} ) ); } - $verbose and warn sprintf "Found %s borrowers with overdues\n", $sth->rows; - my $borrowernumber; - # Iterate over all overdue issues - while ( my $data = $sth->fetchrow_hashref ) { - - # check the borrower has at least one item that matches - my $days_between; - if ( C4::Context->preference('OverdueNoticeCalendar') ) - { - $days_between = - $calendar->days_between( dt_from_string($data->{date_due}), - $date_to_run ); - } - else { - $days_between = - $date_to_run->delta_days( dt_from_string($data->{date_due}) ); + $days_between = $days_between->in_units('days'); + if ($listall) { + unless ( $days_between >= 1 and $days_between <= $MAX ) { + next; } - $days_between = $days_between->in_units('days'); + } else { if ($triggered) { if ( $mindays != $days_between ) { + $verbose and warn "Overdue skipped for trigger $i\n"; next; } - } - else { - unless ( $days_between >= $mindays + } else { + unless ( $days_between >= $mindays && $days_between <= $maxdays ) { + $verbose and warn "Overdue skipped for trigger $i\n"; next; } } - if (defined $borrowernumber && $borrowernumber eq $data->{'borrowernumber'}){ -# we have already dealt with this borrower - $verbose and warn "already dealt with this borrower $borrowernumber"; - next; - } - $borrowernumber = $data->{'borrowernumber'}; - - # Skip duplicate borrowers as we'll have already handled them - next if ( $patron_homelibrary && $already_queued{"$borrowernumber$i"} ); + } - my $borr = sprintf( "%s%s%s (%s)", + if ($verbose) { + my $borr = sprintf( + "%s%s%s (%s)", $data->{'surname'} || '', $data->{'firstname'} && $data->{'surname'} ? ', ' : '', $data->{'firstname'} || '', - $borrowernumber ); - $verbose and warn "borrower $borr has items triggering level $i.\n"; - - my $patron = Koha::Patrons->find( $borrowernumber ); - if ($patron_homelibrary) { - $branchcode = $patron->branchcode; - $library = Koha::Libraries->find($branchcode); - $admin_email_address = $library->from_email_address; - $branch_email_address = C4::Context->preference('AddressForFailedOverdueNotices') - || $library->inbound_email_address; - } - @emails_to_use = (); - my $notice_email = $patron->notice_email_address; - unless ($nomail) { - if (@emails) { - foreach (@emails) { - push @emails_to_use, $data->{$_} if ( $data->{$_} ); - } - } - else { - push @emails_to_use, $notice_email if ($notice_email); - } - } - - my $letter = Koha::Notice::Templates->find_effective_template( - { - module => 'circulation', - code => $overdue_rules->{"overdue_$i".'_notice'}, - branchcode => $branchcode, - lang => $patron->lang - } + $data->{borrowernumber} ); + warn sprintf "Overdue matched trigger %s with delay of %s days and overdue due date of %s\n", + $i, + $triggered, $overdue_rules->{ "overdue_$i" . '_delay' }, $data->{date_due}; + warn sprintf "Using letter code '%s'\n", + $overdue_rules->{ "overdue_$i" . '_notice' }; + } - unless ($letter) { - $verbose and warn qq|Message '$overdue_rules->{"overdue_$i"."_notice"}' content not found|; - - # might as well skip while PERIOD, no other borrowers are going to work. - # FIXME : Does this mean a letter must be defined in order to trigger a debar ? - next PERIOD; - } - - if ( $overdue_rules->{"overdue_$i".'_restrict'} ) { - - #action taken is debarring - AddUniqueDebarment( - { - borrowernumber => $borrowernumber, - type => 'OVERDUES', - comment => "OVERDUES_PROCESS " . output_pref( dt_from_string() ), - } - ) unless $test_mode; - $verbose and warn "debarring $borr\n"; - } - my @params = $patron_homelibrary ? ($borrowernumber) : ( $borrowernumber, $branchcode ); - - $sth2->execute(@params); - my $itemcount = 0; - my $titles = ""; - my @items = (); - - my $j = 0; - my $exceededPrintNoticesMaxLines = 0; - # Get each overdue item for this patron - while ( my $item_info = $sth2->fetchrow_hashref() ) { - if ( C4::Context->preference('OverdueNoticeCalendar') ) { - $days_between = - $calendar->days_between( - dt_from_string( $item_info->{date_due} ), $date_to_run ); - } - else { - $days_between = - $date_to_run->delta_days( - dt_from_string( $item_info->{date_due} ) ); - } - $days_between = $days_between->in_units('days'); - if ($listall){ - unless ($days_between >= 1 and $days_between <= $MAX){ - next; - } - } - else { - if ($triggered) { - if ( $mindays != $days_between ) { - next; - } - } - else { - unless ( $days_between >= $mindays - && $days_between <= $maxdays ) - { - next; - } - } - } - - if ( ( scalar(@emails_to_use) == 0 || $nomail ) && $PrintNoticesMaxLines && $j >= $PrintNoticesMaxLines ) { - $exceededPrintNoticesMaxLines = 1; - last; - } - next if $patron_homelibrary and !grep { $seen{ $item_info->{branchcode} } } @branches; - $j++; - - $titles .= C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields, dateonly => 1 } ); - $itemcount++; - push @items, $item_info; - } - $sth2->finish; - - my @message_transport_types = split( /,/, $overdue_rules->{ "overdue_$i" . '_mtt' } ); - - my $print_sent = 0; # A print notice is not yet sent for this patron - for my $mtt ( @message_transport_types ) { - next if $mtt eq 'itiva'; - my $effective_mtt = $mtt; - if ( ($mtt eq 'email' and not scalar @emails_to_use) or ($mtt eq 'sms' and not $data->{smsalertnumber}) ) { - # email or sms is requested but not exist, do a print. - $effective_mtt = 'print'; - } - splice @items, $PrintNoticesMaxLines if $effective_mtt eq 'print' && $PrintNoticesMaxLines && scalar @items > $PrintNoticesMaxLines; - #catch the case where we are sending a print to someone with an email - - my $letter_exists = Koha::Notice::Templates->find_effective_template( - { - module => 'circulation', - code => $overdue_rules->{ "overdue_$i" . '_notice' }, - message_transport_type => $effective_mtt, - branchcode => $branchcode, - lang => $patron->lang - } - ); - - my $letter = parse_overdues_letter( - { - letter_code => $overdue_rules->{ "overdue_$i" . '_notice' }, - borrowernumber => $borrowernumber, - branchcode => $branchcode, - items => \@items, - substitute => { # this appears to be a hack to overcome incomplete features in this code. - bib => $library->branchname, # maybe 'bib' is a typo for 'lib'? - 'items.content' => $titles, - 'count' => $itemcount, - }, - - # If there is no template defined for the requested letter - # Fallback on the original type - message_transport_type => $letter_exists ? $effective_mtt : $mtt, - } - ); - unless ( $letter && $letter->{content} ) { - $verbose and warn qq|Message '$overdue_rules->{"overdue_$i"."_notice"}' content not found|; - - # this transport doesn't have a configured notice, so try another - next; - } - - if ( $exceededPrintNoticesMaxLines ) { - $letter->{'content'} .= "List too long for form; please check your account online for a complete list of your overdue items."; - } - - my @misses = grep { /./ } map { /^([^>]*)[>]+/; ( $1 || '' ); } split /\{'content'}; - if (@misses) { - $verbose and warn "The following terms were not matched and replaced: \n\t" . join "\n\t", @misses; - } - - if ($nomail) { - push @output_chunks, - prepare_letter_for_printing( - { letter => $letter, - borrowernumber => $borrowernumber, - firstname => $data->{'firstname'}, - lastname => $data->{'surname'}, - address1 => $data->{'address'}, - address2 => $data->{'address2'}, - city => $data->{'city'}, - phone => $data->{'phone'}, - cardnumber => $data->{'cardnumber'}, - branchname => $library->branchname, - letternumber => $i, - postcode => $data->{'zipcode'}, - country => $data->{'country'}, - email => $notice_email, - itemcount => $itemcount, - titles => $titles, - outputformat => defined $csvfilename ? 'csv' : defined $htmlfilename ? 'html' : defined $text_filename ? 'text' : '', - } - ); - } else { - if ( ($mtt eq 'email' and not scalar @emails_to_use) or ($mtt eq 'sms' and not $data->{smsalertnumber}) ) { - push @output_chunks, - prepare_letter_for_printing( - { letter => $letter, - borrowernumber => $borrowernumber, - firstname => $data->{'firstname'}, - lastname => $data->{'surname'}, - address1 => $data->{'address'}, - address2 => $data->{'address2'}, - city => $data->{'city'}, - postcode => $data->{'zipcode'}, - country => $data->{'country'}, - email => $notice_email, - itemcount => $itemcount, - titles => $titles, - outputformat => defined $csvfilename ? 'csv' : defined $htmlfilename ? 'html' : defined $text_filename ? 'text' : '', - } - ); - } - unless ( $effective_mtt eq 'print' and $print_sent == 1 ) { - # Just sent a print if not already done. - C4::Letters::EnqueueLetter( - { letter => $letter, - borrowernumber => $borrowernumber, - message_transport_type => $effective_mtt, - from_address => $admin_email_address, - to_address => join(',', @emails_to_use), - reply_address => $library->inbound_email_address, - } - ) unless $test_mode; - # A print notice should be sent only once per overdue level. - # 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. - $print_sent = 1 if $effective_mtt eq 'print'; - } - } - } - $already_queued{"$borrowernumber$i"} = 1; + my @message_transport_types = split( /,/, $overdue_rules->{ "overdue_$i" . '_mtt' } ); + for my $mtt (@message_transport_types) { + push @{ $borrower_overdues->{triggers}->{$i}->{ $overdue_rules->{ "overdue_$i" . '_notice' } } + ->{$mtt} }, $data; } - $sth->finish; + if ( $overdue_rules->{ "overdue_$i" . '_restrict' } ) { + $borrower_overdues->{restrict} = 1; + } + $borrower_overdues->{'email'} = $data->{'email'}; + $borrower_overdues->{'emailpro'} = $data->{'emailpro'}; + $borrower_overdues->{'B_email'} = $data->{'B_email'}; + $borrower_overdues->{'smsalertnumber'} = $data->{'smsalertnumber'}; + $borrower_overdues->{'phone'} = $data->{'phone'}; } } + $sth->finish; + + # Catch final trigger + if ( $borrower_overdues->{borrowernumber} ) { + $verbose and warn "Collected overdue triggers for " . $borrower_overdues->{borrowernumber} . "\n"; + _enact_trigger($borrower_overdues); + $borrower_overdues = {}; + } } if (@output_chunks) { if ( defined $csvfilename ) { print $csv_fh @output_chunks; - } - elsif ( defined $htmlfilename ) { + } elsif ( defined $htmlfilename ) { print $fh @output_chunks; - } - elsif ( defined $text_filename ) { + } elsif ( defined $text_filename ) { print $fh @output_chunks; + } elsif ($nomail) { + local $, = "\f"; # pagebreak + print @output_chunks; } - elsif ($nomail){ - local $, = "\f"; # pagebreak - print @output_chunks; - } + # Generate the content of the csv with headers my $content; if ( defined $csvfilename ) { my $delimiter = C4::Context->csv_delimiter; - $content = join($delimiter, qw(title name surname address1 address2 zipcode city country email itemcount itemsinfo due_date issue_date)) . "\n"; + $content = join( + $delimiter, + qw(title name surname address1 address2 zipcode city country email itemcount itemsinfo due_date issue_date) + ) . "\n"; $content .= join( "\n", @output_chunks ); } elsif ( defined $htmlfilename ) { $content = _get_html_start(); @@ -907,8 +766,10 @@ END_SQL if ( C4::Context->preference('EmailOverduesNoEmail') ) { my $attachment = { - filename => defined $csvfilename ? 'attachment.csv' : defined $htmlfilename ? 'attachment.html' : 'attachment.txt', - type => defined $htmlfilename ? 'text/html' : 'text/plain', + filename => defined $csvfilename ? 'attachment.csv' + : defined $htmlfilename ? 'attachment.html' + : 'attachment.txt', + type => defined $htmlfilename ? 'text/html' : 'text/plain', content => $content, }; @@ -918,7 +779,8 @@ END_SQL }; C4::Letters::EnqueueLetter( - { letter => $letter, + { + letter => $letter, borrowernumber => undef, message_transport_type => 'email', attachments => [$attachment], @@ -927,25 +789,252 @@ END_SQL ) unless $test_mode; } } - } + if ($csvfilename) { + # note that we're not testing on $csv_fh to prevent closing # STDOUT. close $csv_fh; } if ( defined $htmlfilename ) { - print $fh _get_html_end(); - close $fh; + print $fh _get_html_end(); + close $fh; } elsif ( defined $text_filename ) { - close $fh; + close $fh; } =head1 INTERNAL METHODS These methods are internal to the operation of overdue_notices.pl. +=cut + +sub _enact_trigger { + my ($borrower_overdues) = @_; + + my $borrowernumber = $borrower_overdues->{borrowernumber}; + my $branchcode = $borrower_overdues->{branchcode}; + my $patron = Koha::Patrons->find($borrowernumber); + my ( $library, $admin_email_address, $branch_email_address ); + $library = Koha::Libraries->find($branchcode); + + if ($patron_homelibrary) { + $branchcode = $patron->branchcode; + $library = Koha::Libraries->find($branchcode); + $admin_email_address = $library->from_email_address; + $branch_email_address = C4::Context->preference('AddressForFailedOverdueNotices') + || $library->inbound_email_address; + } + @emails_to_use = (); + my $notice_email = $patron->notice_email_address; + unless ($nomail) { + if (@emails) { + foreach (@emails) { + push @emails_to_use, $borrower_overdues->{$_} if ( $borrower_overdues->{$_} ); + } + } else { + push @emails_to_use, $notice_email if ($notice_email); + } + } + + for my $trigger ( sort keys %{ $borrower_overdues->{triggers} } ) { + for my $notice ( keys %{ $borrower_overdues->{triggers}->{$trigger} } ) { + my $print_sent = 0; # A print notice is not yet sent for this patron + for my $mtt ( keys %{ $borrower_overdues->{triggers}->{$trigger}->{$notice} } ) { + + next if $mtt eq 'itiva'; + my $effective_mtt = $mtt; + if ( ( $mtt eq 'email' and not scalar @emails_to_use ) + or ( $mtt eq 'sms' and not $borrower_overdues->{smsalertnumber} ) ) + { + # email or sms is requested but not exist, do a print. + $effective_mtt = 'print'; + } + + my $j = 0; + my $exceededPrintNoticesMaxLines = 0; + + # Get each overdue item for this trigger + my $itemcount = 0; + my $titles = ""; + my @items = (); + for my $item_info ( @{ $borrower_overdues->{triggers}->{$trigger}->{$notice}->{$effective_mtt} } ) { + if ( ( scalar(@emails_to_use) == 0 || $nomail ) + && $PrintNoticesMaxLines + && $j >= $PrintNoticesMaxLines ) + { + $exceededPrintNoticesMaxLines = 1; + last; + } + next if $patron_homelibrary and !grep { $seen{ $item_info->{branchcode} } } @branches; + $j++; + + $titles .= C4::Letters::get_item_content( + { item => $item_info, item_content_fields => \@item_content_fields, dateonly => 1 } ); + $itemcount++; + push @items, $item_info; + } + + splice @items, $PrintNoticesMaxLines + if $effective_mtt eq 'print' + && $PrintNoticesMaxLines + && scalar @items > $PrintNoticesMaxLines; + + #catch the case where we are sending a print to someone with an email + + my $letter_exists = Koha::Notice::Templates->find_effective_template( + { + module => 'circulation', + code => $notice, + message_transport_type => $effective_mtt, + branchcode => $branchcode, + lang => $patron->lang + } + ); + + unless ($letter_exists) { + $verbose and warn qq|Message '$notice' for '$effective_mtt' content not found|; + next; + } + + my $letter = parse_overdues_letter( + { + letter_code => $notice, + borrowernumber => $borrowernumber, + branchcode => $branchcode, + items => \@items, + substitute => { # this appears to be a hack to overcome incomplete features in this code. + bib => $library->branchname, # maybe 'bib' is a typo for 'lib'? + 'items.content' => $titles, + 'count' => $itemcount, + }, + + # If there is no template defined for the requested letter + # Fallback on the original type + message_transport_type => $letter_exists ? $effective_mtt : $mtt, + } + ); + unless ( $letter && $letter->{content} ) { + $verbose and warn qq|Message '$notice' content not found|; + + # this transport doesn't have a configured notice, so try another + next; + } + + if ($exceededPrintNoticesMaxLines) { + $letter->{'content'} .= + "List too long for form; please check your account online for a complete list of your overdue items."; + } + + my @misses = grep { /./ } map { /^([^>]*)[>]+/; ( $1 || '' ); } split /\{'content'}; + if (@misses) { + $verbose + and warn "The following terms were not matched and replaced: \n\t" . join "\n\t", + @misses; + } + + if ($nomail) { + push @output_chunks, + prepare_letter_for_printing( + { + letter => $letter, + borrowernumber => $borrowernumber, + firstname => $borrower_overdues->{'firstname'}, + lastname => $borrower_overdues->{'surname'}, + address1 => $borrower_overdues->{'address'}, + address2 => $borrower_overdues->{'address2'}, + city => $borrower_overdues->{'city'}, + phone => $borrower_overdues->{'phone'}, + cardnumber => $borrower_overdues->{'cardnumber'}, + branchname => $library->branchname, + letternumber => $trigger, + postcode => $borrower_overdues->{'zipcode'}, + country => $borrower_overdues->{'country'}, + email => $notice_email, + itemcount => $itemcount, + titles => $titles, + outputformat => defined $csvfilename ? 'csv' + : defined $htmlfilename ? 'html' + : defined $text_filename ? 'text' + : '', + } + ); + } else { + if ( ( $mtt eq 'email' and not scalar @emails_to_use ) + or ( $mtt eq 'sms' and not $borrower_overdues->{smsalertnumber} ) ) + { + push @output_chunks, + prepare_letter_for_printing( + { + letter => $letter, + borrowernumber => $borrowernumber, + firstname => $borrower_overdues->{'firstname'}, + lastname => $borrower_overdues->{'surname'}, + address1 => $borrower_overdues->{'address'}, + address2 => $borrower_overdues->{'address2'}, + city => $borrower_overdues->{'city'}, + postcode => $borrower_overdues->{'zipcode'}, + country => $borrower_overdues->{'country'}, + email => $notice_email, + itemcount => $itemcount, + titles => $titles, + outputformat => defined $csvfilename ? 'csv' + : defined $htmlfilename ? 'html' + : defined $text_filename ? 'text' + : '', + } + ); + } + unless ( $effective_mtt eq 'print' and $print_sent == 1 ) { + + # Just sent a print if not already done. + C4::Letters::EnqueueLetter( + { + letter => $letter, + borrowernumber => $borrowernumber, + message_transport_type => $effective_mtt, + from_address => $admin_email_address, + to_address => join( ',', @emails_to_use ), + reply_address => $library->inbound_email_address, + } + ) unless $test_mode; + + # A print notice should be sent only once per overdue level. + # 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. + $print_sent = 1 if $effective_mtt eq 'print'; + } + } + } + + $already_queued{"$borrowernumber$trigger"} = 1; + } + } + + if ( $borrower_overdues->{restrict} ) { + + #action taken is debarring + AddUniqueDebarment( + { + borrowernumber => $borrowernumber, + type => 'OVERDUES', + comment => "OVERDUES_PROCESS " . output_pref( dt_from_string() ), + } + ) unless $test_mode; + + my $borr = sprintf( + "%s%s%s (%s)", + $borrower_overdues->{'surname'} || '', + $borrower_overdues->{'firstname'} && $borrower_overdues->{'surname'} ? ', ' : '', + $borrower_overdues->{'firstname'} || '', + $borrower_overdues->{borrowernumber} + ); + $verbose and warn "debarring $borr\n"; + } +} + =head2 prepare_letter_for_printing returns a string of text appropriate for printing in the event that an @@ -974,20 +1063,24 @@ sub prepare_letter_for_printing { my $return; chomp $params->{titles}; if ( exists $params->{'outputformat'} && $params->{'outputformat'} eq 'csv' ) { - if ($csv->combine( - $params->{'firstname'}, $params->{'lastname'}, $params->{'address1'}, $params->{'address2'}, $params->{'postcode'}, - $params->{'city'}, $params->{'country'}, $params->{'email'}, $params->{'phone'}, $params->{'cardnumber'}, + if ( + $csv->combine( + $params->{'firstname'}, $params->{'lastname'}, $params->{'address1'}, $params->{'address2'}, + $params->{'postcode'}, + $params->{'city'}, $params->{'country'}, $params->{'email'}, $params->{'phone'}, + $params->{'cardnumber'}, $params->{'itemcount'}, $params->{'titles'}, $params->{'branchname'}, $params->{'letternumber'} ) - ) { + ) + { return $csv->string, "\n"; } else { $verbose and warn 'combine failed on argument: ' . $csv->error_input; } } elsif ( exists $params->{'outputformat'} && $params->{'outputformat'} eq 'html' ) { - $return = "
\n";
-      $return .= "$params->{'letter'}->{'content'}\n";
-      $return .= "\n
\n"; + $return = "
\n";
+        $return .= "$params->{'letter'}->{'content'}\n";
+        $return .= "\n
\n"; } else { $return .= "$params->{'letter'}->{'content'}\n"; @@ -1034,4 +1127,4 @@ sub _get_html_end { } -cronlogaction({ action => 'End', info => "COMPLETED" }); +cronlogaction( { action => 'End', info => "COMPLETED" } ); -- 2.47.0