From 8867219ab669bd960bf4d109e91f5664ea39a7d8 Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Thu, 8 Aug 2024 11:44:35 +0100 Subject: [PATCH] Bug 10190: Rework logic of overdue_notices This patch enhances the overdue_notices script to understand the newly introduced itemtype level for overdue rules. 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. Sponsored-by: Glasgow Colleges Library Group Signed-off-by: George Harkins --- misc/cronjobs/overdue_notices.pl | 826 +++++++++++++++++-------------- 1 file changed, 464 insertions(+), 362 deletions(-) diff --git a/misc/cronjobs/overdue_notices.pl b/misc/cronjobs/overdue_notices.pl index 6e4aa782184..a8205a1273c 100755 --- a/misc/cronjobs/overdue_notices.pl +++ b/misc/cronjobs/overdue_notices.pl @@ -29,7 +29,7 @@ use DateTime::Duration; use Koha::Script -cron; use C4::Context; use C4::Letters; -use C4::Overdues qw( GetOverdueMessageTransportTypes parse_overdues_letter ); +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 ); @@ -142,6 +142,14 @@ defaults to due date,title,barcode,author,itemnumber Other possible values come from fields in the biblios, items and issues tables. +=item B<--itemtypes> + +Repeatable field, that permits to select only some item types. + +=item B<--itemtypesout> + +Repeatable field, that permits to exclude some item types. + =item B<--borcat> Repeatable field that permits selecting only specific patron categories. @@ -156,7 +164,7 @@ patrons in the STAFF category. =item B<-t> | B<--triggered> -This option causes a notice to be generated if and only if +This option causes a notice to be generated if and only if an item is overdue by the number of days defined in a notice trigger. By default, a notice is sent each time the script runs, which is suitable for @@ -175,7 +183,7 @@ production data. =item B<--list-all> -Default items.content lists only those items that fall in the +Default items.content lists only those items that fall in the range of the currently processing notice. Choose --list-all to include all overdue items in the list (limited by B<--max> setting). @@ -321,14 +329,15 @@ 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 $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 @@ -338,6 +347,8 @@ my $text_filename; my $triggered = 0; my $listall = 0; my $itemscontent = join( ',', qw( date_due title barcode author itemnumber ) ); +my @myitemtypes; +my @myitemtypesout; my @myborcat; my @myborcatout; my ( $date_input, $today ); @@ -356,6 +367,8 @@ GetOptions( 'html:s' => \$htmlfilename, # this optional argument gets '' if not supplied. 'text:s' => \$text_filename, # this optional argument gets '' if not supplied. 'itemscontent=s' => \$itemscontent, + 'itemtypes=s' => \@myitemtypes, + 'itemtypeouts=s' => \@myitemtypesout, 'list-all' => \$listall, 't|triggered' => \$triggered, 'test' => \$test_mode, @@ -462,6 +475,8 @@ if ( defined $csvfilename ) { } @branches = @overduebranches unless @branches; + +# Setup output file if requested our $fh; if ( defined $htmlfilename ) { if ( $htmlfilename eq '' ) { @@ -481,8 +496,33 @@ if ( defined $htmlfilename ) { } } +# 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'); +} + +# 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') ) { @@ -496,116 +536,165 @@ foreach my $branchcode (@branches) { 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. + @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 $query = "SELECT * FROM overduerules WHERE delay1 IS NOT NULL AND branchcode = ? "; - $query .= " AND categorycode IN (" . join( ',', ('?') x @myborcat ) . ") " if (@myborcat); - $query .= " AND categorycode NOT IN (" . join( ',', ('?') x @myborcatout ) . ") " if (@myborcatout); - - my $rqoverduerules = $dbh->prepare($query); - $rqoverduerules->execute( $branchcode, @myborcat, @myborcatout ); - - # We get default rules if there is no rule for this branch - if ( $rqoverduerules->rows == 0 ) { - $query = "SELECT * FROM overduerules WHERE delay1 IS NOT NULL AND branchcode = '' "; - $query .= " AND categorycode IN (" . join( ',', ('?') x @myborcat ) . ") " if (@myborcat); - $query .= " AND categorycode NOT IN (" . join( ',', ('?') x @myborcatout ) . ") " if (@myborcatout); + 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; - $rqoverduerules = $dbh->prepare($query); - $rqoverduerules->execute( @myborcat, @myborcatout ); - } + my $borrowernumber; + my $borrower_overdues = {}; - # my $outfile = 'overdues_' . ( $mybranch || $branchcode || 'default' ); - while ( my $overdue_rules = $rqoverduerules->fetchrow_hashref ) { - PERIOD: foreach my $i ( 1 .. 3 ) { + # Iterate over all overdues + OVERDUE: while ( my $data = $sth->fetchrow_hashref ) { + my $itemtype = $data->{itype} // $data->{b_itemtype}; - $verbose and warn "branch '$branchcode', categorycode = $overdue_rules->{categorycode} pass $i\n"; + # 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 }; + } - my $mindays = $overdue_rules->{"delay$i"}; # the notice will be sent after mindays days (grace period) - my $maxdays = ( - $overdue_rules->{ "delay" . ( $i + 1 ) } - ? $overdue_rules->{ "delay" . ( $i + 1 ) } - 1 - : ($MAX) - ); # issues being more than maxdays late are managed somewhere else. (borrower probably suspended) + $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; + PERIOD: while (1) { + $i++; + my $ii = $i + 1; + my $overdue_rules = Koha::CirculationRules->get_effective_rules( + { + rules => [ + "overdue_$i" . '_delay', "overdue_$i" . '_notice', "overdue_$i" . '_mtt', + "overdue_$i" . '_restrict', "overdue_$ii" . '_delay' + ], + categorycode => $category, + branchcode => $branchcode, + itemtype => $itemtype, + } + ); - next unless defined $mindays; + if ( !defined( $overdue_rules->{ "overdue_$i" . '_delay' } ) ) { + last PERIOD; + } - if ( !$overdue_rules->{"letter$i"} ) { - $verbose and warn sprintf "No letter code found for pass %s\n", $i; - next PERIOD; - } - $verbose and warn sprintf "Using letter code '%s' for pass %s\n", $overdue_rules->{"letter$i"}, $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). - # - - 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=? '; + my $mindays = + $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) + + if ( !$overdue_rules->{ "overdue_$i" . '_notice' } ) { + $verbose and warn sprintf "Trigger %s skipped, No letter code found\n", $i; + next PERIOD; } - push @borrower_parameters, $branchcode; - } - if ( $overdue_rules->{categorycode} ) { - $borrower_sql .= ' AND borrowers.categorycode=? '; - push @borrower_parameters, $overdue_rules->{categorycode}; - } - $borrower_sql .= ' AND categories.overduenoticerequired=1 ORDER BY issues.borrowernumber'; - - # $sth gets borrower info iff 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 | " - . $overdue_rules->{'categorycode'} - . "\n ($mindays, $maxdays, " - . $date_to_run->datetime() . ")\n"; - warn sprintf "--------------------------\n"; - } - $verbose and warn sprintf "Found %s borrowers with overdues\n", $sth->rows; - my $borrowernumber; - while ( my $data = $sth->fetchrow_hashref ) { - # check the borrower has at least one item that matches + # $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). + # + + # Check the overdue period matches my $days_between; if ( C4::Context->preference('OverdueNoticeCalendar') ) { $days_between = $calendar->days_between( @@ -617,276 +706,63 @@ END_SQL $date_to_run->delta_days( dt_from_string( $data->{date_due} ) ); } $days_between = $days_between->in_units('days'); - if ($triggered) { - if ( $mindays != $days_between ) { + if ($listall) { + unless ( $days_between >= 1 and $days_between <= $MAX ) { next; } } else { - unless ( $days_between >= $mindays - && $days_between <= $maxdays ) - { - 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'}; - next if ( $patron_homelibrary && $already_queued{"$borrowernumber$i"} ); - 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->{$_} ); + if ($triggered) { + if ( $mindays != $days_between ) { + $verbose and warn "Overdue skipped for trigger $i\n"; + next; } } else { - push @emails_to_use, $notice_email if ($notice_email); - } - } - - my $letter = Koha::Notice::Templates->find_effective_template( - { - module => 'circulation', - code => $overdue_rules->{"letter$i"}, - branchcode => $branchcode, - lang => $patron->lang - } - ); - - unless ($letter) { - $verbose and warn qq|Message '$overdue_rules->{"letter$i"}' 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->{"debarred$i"} ) { - - #action taken is debarring - AddUniqueDebarment( + unless ( $days_between >= $mindays + && $days_between <= $maxdays ) { - 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; - 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 ) { + $verbose and warn "Overdue skipped for trigger $i\n"; 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 = - @{ GetOverdueMessageTransportTypes( $branchcode, $overdue_rules->{categorycode}, $i ) }; - @message_transport_types = - @{ GetOverdueMessageTransportTypes( q{}, $overdue_rules->{categorycode}, $i ) } - unless @message_transport_types; - - 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->{"letter$i"}, - message_transport_type => $effective_mtt, - branchcode => $branchcode, - lang => $patron->lang - } + if ($verbose) { + my $borr = sprintf( + "%s%s%s (%s)", + $data->{'surname'} || '', + $data->{'firstname'} && $data->{'surname'} ? ', ' : '', + $data->{'firstname'} || '', + $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' }; + } - my $letter = parse_overdues_letter( - { - letter_code => $overdue_rules->{"letter$i"}, - 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->{"letter$i"}' 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'; - } - } + 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; } - $already_queued{"$borrowernumber$i"} = 1; + 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; + } + $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 = {}; } } @@ -944,8 +820,8 @@ END_SQL ) unless $test_mode; } } - } + if ($csvfilename) { # note that we're not testing on $csv_fh to prevent closing @@ -964,6 +840,232 @@ if ( defined $htmlfilename ) { 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 -- 2.51.0