=head1 NAME
-advance_notices.pl - cron script to put item due reminders into message queue
+advance_notices.pl - prepare messages to be sent to patrons for nearly due, or due, items
=head1 SYNOPSIS
-./advance_notices.pl -c
-
-or, in crontab:
-0 1 * * * advance_notices.pl -c
+ advance_notices.pl
+ [ -n ][ -m <number of days> ][ --itemscontent <comma separated field list> ][ -c ]
=head1 DESCRIPTION
use strict;
use warnings;
-use Getopt::Long;
-use Pod::Usage;
-use Data::Dumper;
-BEGIN {
- # find Koha's Perl modules
- # test carefully before changing this
- use FindBin;
- eval { require "$FindBin::Bin/../kohalib.pl" };
-}
-use C4::Biblio;
+use Getopt::Long qw( GetOptions );
+use Pod::Usage qw( pod2usage );
+use Koha::Script -cron;
use C4::Context;
use C4::Letters;
use C4::Members;
use C4::Members::Messaging;
-use C4::Overdues;
-use Koha::DateUtils;
-use C4::Log;
-
-=head1 NAME
-
-advance_notices.pl - prepare messages to be sent to patrons for nearly due, or due, items
-
-=head1 SYNOPSIS
-
-advance_notices.pl
- [ -n ][ -m <number of days> ][ --itemscontent <comma separated field list> ][ -c ]
+use C4::Log qw( cronlogaction );
+use Koha::Items;
+use Koha::Libraries;
+use Koha::Patrons;
=head1 OPTIONS
comma separated list of fields that get substituted into templates in
places of the E<lt>E<lt>items.contentE<gt>E<gt> placeholder. This
-defaults to due date,title,author,barcode
+defaults to date_due,title,author,barcode
Other possible values come from fields in the biblios, items and
issues tables.
-=back
+=item B<--digest-per-branch>
-=head1 DESCRIPTION
+Flag to indicate that generation of message digests should be
+performed separately for each branch.
+
+A patron could potentially have loans at several different branches
+There is no natural branch to set as the sender on the aggregated
+message in this situation so the default behavior is to use the
+borrowers home branch. This could surprise to the borrower when
+message sender is a library where they have not borrowed anything.
+
+Enabling this flag ensures that the issuing library is the sender of
+the digested message. It has no effect unless the borrower has
+chosen 'Digests only' on the advance messages.
+
+=item B<--library>
-This script is designed to alert patrons when items are due, or coming due
+select notices for one specific library. Use the value in the
+branches.branchcode table. This option can be repeated in order
+to select notices for a group of libraries.
+
+=item B<--frombranch>
+
+Set the from address for the notice to one of 'item-homebranch' or 'item-issuebranch'.
+
+Defaults to 'item-issuebranch'
+
+=back
=head2 Configuration
=item E<lt>E<lt>items.contentE<gt>E<gt>
one line for each item, each line containing a tab separated list of
-title, author, barcode, issuedate
+date due, title, author, barcode
=item E<lt>E<lt>borrowers.*E<gt>E<gt>
The F<misc/cronjobs/overdue_notices.pl> program allows you to send
messages to patrons when their messages are overdue.
+
=cut
+binmode( STDOUT, ':encoding(UTF-8)' );
+
# These are defaults for command line options.
my $confirm; # -c: Confirm that the user has read and configured this script.
my $nomail; # -n: No mail. Will not send any emails.
my $mindays = 0; # -m: Maximum number of days in advance to send notices
my $maxdays = 30; # -e: the End of the time period
my $verbose = 0; # -v: verbose
+my $digest_per_branch = 0; # -digest-per-branch: Prepare and send digests per branch
+my @branchcodes; # Branch(es) passed as parameter
+my $frombranch = 'item-issuebranch';
my $itemscontent = join(',',qw( date_due title author barcode ));
my $help = 0;
my $man = 0;
+my $command_line_options = join(" ",@ARGV);
+
GetOptions(
'help|?' => \$help,
'man' => \$man,
+ 'library=s' => \@branchcodes,
+ 'frombranch=s' => \$frombranch,
'c' => \$confirm,
'n' => \$nomail,
'm:i' => \$maxdays,
'v' => \$verbose,
+ 'digest-per-branch' => \$digest_per_branch,
'itemscontent=s' => \$itemscontent,
)or pod2usage(2);
pod2usage(1) if $help;
-pod2usage( -verbose => 2 ) if $man;;
+pod2usage( -verbose => 2 ) if $man;
# Since advance notice options are not visible in the web-interface
# unless EnhancedMessagingPreferences is on, let the user know that
# this script probably isn't going to do much
-if ( ! C4::Context->preference('EnhancedMessagingPreferences') ) {
+if ( ! C4::Context->preference('EnhancedMessagingPreferences') && $verbose ) {
warn <<'END_WARN';
The "EnhancedMessagingPreferences" syspref is off.
unless ($confirm) {
pod2usage(1);
}
+cronlogaction({ info => $command_line_options });
+
+my %branches = ();
+if (@branchcodes) {
+ %branches = map { $_ => 1 } @branchcodes;
+}
-cronlogaction();
+die "--frombranch takes item-homebranch or item-issuebranch only"
+ unless ( $frombranch eq 'item-issuebranch'
+ || $frombranch eq 'item-homebranch' );
+my $owning_library = ( $frombranch eq 'item-homebranch' ) ? 1 : 0;
# The fields that will be substituted into <<items.content>>
my @item_content_fields = split(/,/,$itemscontent);
warn 'getting upcoming due issues' if $verbose;
-my $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( { days_in_advance => $maxdays } );
+my $upcoming_dues = C4::Circulation::GetUpcomingDueIssues( {
+ days_in_advance => $maxdays,
+ owning_library => $owning_library
+ } );
warn 'found ' . scalar( @$upcoming_dues ) . ' issues' if $verbose;
# hash of borrowernumber to number of items upcoming
# for patrons wishing digests only.
-my $upcoming_digest;
-my $due_digest;
+my $upcoming_digest = {};
+my $due_digest = {};
my $dbh = C4::Context->dbh();
my $sth = $dbh->prepare(<<'END_SQL');
if ( $borrower_preferences->{'wants_digest'} ) {
# cache this one to process after we've run through all of the items.
- $due_digest->{ $upcoming->{borrowernumber} }->{email} = $from_address;
- $due_digest->{ $upcoming->{borrowernumber} }->{count}++;
+ if ($digest_per_branch) {
+ $due_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{email} = $from_address;
+ $due_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{count}++;
+ } else {
+ $due_digest->{ $upcoming->{borrowernumber} }->{email} = $from_address;
+ $due_digest->{ $upcoming->{borrowernumber} }->{count}++;
+ }
} else {
- my $biblio = C4::Biblio::GetBiblioFromItemNumber( $upcoming->{'itemnumber'} );
+ my $branchcode;
+ if($owning_library) {
+ $branchcode = $upcoming->{'homebranch'};
+ } else {
+ $branchcode = $upcoming->{'branchcode'};
+ }
+ # Skip this DUE if we specify list of libraries and this one is not part of it
+ next if (@branchcodes && !$branches{$branchcode});
+
+ my $item = Koha::Items->find( $upcoming->{itemnumber} );
my $letter_type = 'DUE';
$sth->execute($upcoming->{'borrowernumber'},$upcoming->{'itemnumber'},'0');
my $titles = "";
while ( my $item_info = $sth->fetchrow_hashref()) {
- my @item_info = map { $_ =~ /^date|date$/ ? format_date($item_info->{$_}) : $item_info->{$_} || '' } @item_content_fields;
- $titles .= join("\t",@item_info) . "\n";
+ $titles .= C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields } );
}
## Get branch info for borrowers home library.
foreach my $transport ( keys %{$borrower_preferences->{'transports'}} ) {
+ next if $transport eq 'itiva';
my $letter = parse_letter( { letter_code => $letter_type,
borrowernumber => $upcoming->{'borrowernumber'},
- branchcode => $upcoming->{'branchcode'},
- biblionumber => $biblio->{'biblionumber'},
+ branchcode => $branchcode,
+ biblionumber => $item->biblionumber,
itemnumber => $upcoming->{'itemnumber'},
substitute => { 'items.content' => $titles },
message_transport_type => $transport,
} )
- or warn "no letter of type '$letter_type' found. Please see sample_notices.sql";
+ or warn "no letter of type '$letter_type' found for borrowernumber ".$upcoming->{'borrowernumber'}.". Please see sample_notices.sql";
push @letters, $letter if $letter;
}
}
if ( $borrower_preferences->{'wants_digest'} ) {
# cache this one to process after we've run through all of the items.
- $upcoming_digest->{ $upcoming->{borrowernumber} }->{email} = $from_address;
- $upcoming_digest->{ $upcoming->{borrowernumber} }->{count}++;
+ if ($digest_per_branch) {
+ $upcoming_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{email} = $from_address;
+ $upcoming_digest->{ $upcoming->{branchcode} }->{ $upcoming->{borrowernumber} }->{count}++;
+ } else {
+ $upcoming_digest->{ $upcoming->{borrowernumber} }->{email} = $from_address;
+ $upcoming_digest->{ $upcoming->{borrowernumber} }->{count}++;
+ }
} else {
- my $biblio = C4::Biblio::GetBiblioFromItemNumber( $upcoming->{'itemnumber'} );
+ my $branchcode;
+ if($owning_library) {
+ $branchcode = $upcoming->{'homebranch'};
+ } else {
+ $branchcode = $upcoming->{'branchcode'};
+ }
+ # Skip this PREDUE if we specify list of libraries and this one is not part of it
+ next if (@branchcodes && !$branches{$branchcode});
+
+ my $item = Koha::Items->find( $upcoming->{itemnumber} );
my $letter_type = 'PREDUE';
$sth->execute($upcoming->{'borrowernumber'},$upcoming->{'itemnumber'},$borrower_preferences->{'days_in_advance'});
my $titles = "";
while ( my $item_info = $sth->fetchrow_hashref()) {
- my @item_info = map { $_ =~ /^date|date$/ ? format_date($item_info->{$_}) : $item_info->{$_} || '' } @item_content_fields;
- $titles .= join("\t",@item_info) . "\n";
+ $titles .= C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields } );
}
## Get branch info for borrowers home library.
foreach my $transport ( keys %{$borrower_preferences->{'transports'}} ) {
+ next if $transport eq 'itiva';
my $letter = parse_letter( { letter_code => $letter_type,
borrowernumber => $upcoming->{'borrowernumber'},
- branchcode => $upcoming->{'branchcode'},
- biblionumber => $biblio->{'biblionumber'},
+ branchcode => $branchcode,
+ biblionumber => $item->biblionumber,
itemnumber => $upcoming->{'itemnumber'},
substitute => { 'items.content' => $titles },
message_transport_type => $transport,
} )
- or warn "no letter of type '$letter_type' found. Please see sample_notices.sql";
+ or warn "no letter of type '$letter_type' found for borrowernumber ".$upcoming->{'borrowernumber'}.". Please see sample_notices.sql";
push @letters, $letter if $letter;
}
}
if ($nomail) {
for my $letter ( @letters ) {
local $, = "\f";
- print $letter->{'content'};
+ print $letter->{'content'}."\n";
}
}
else {
# Now, run through all the people that want digests and send them
-$sth = $dbh->prepare(<<'END_SQL');
+my $sth_digest = $dbh->prepare(<<'END_SQL');
SELECT biblio.*, items.*, issues.*
FROM issues,items,biblio
WHERE items.itemnumber=issues.itemnumber
AND issues.borrowernumber = ?
AND (TO_DAYS(date_due)-TO_DAYS(NOW()) = ?)
END_SQL
-PATRON: while ( my ( $borrowernumber, $digest ) = each %$upcoming_digest ) {
- @letters = ();
- my $count = $digest->{count};
- my $from_address = $digest->{email};
-
- my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber,
- message_name => 'advance_notice' } );
- next PATRON unless $borrower_preferences; # how could this happen?
-
-
- my $letter_type = 'PREDUEDGST';
-
- $sth->execute($borrowernumber,$borrower_preferences->{'days_in_advance'});
- my $titles = "";
- while ( my $item_info = $sth->fetchrow_hashref()) {
- my @item_info = map { $_ =~ /^date|date$/ ? format_date($item_info->{$_}) : $item_info->{$_} || '' } @item_content_fields;
- $titles .= join("\t",@item_info) . "\n";
- }
- ## Get branch info for borrowers home library.
- my %branch_info = get_branch_info( $borrowernumber );
-
- foreach my $transport ( keys %{ $borrower_preferences->{'transports'} } ) {
- my $letter = parse_letter(
- {
- letter_code => $letter_type,
- borrowernumber => $borrowernumber,
- substitute => {
- count => $count,
- 'items.content' => $titles,
- %branch_info,
- },
- branchcode => $branch_info{"branches.branchcode"},
- message_transport_type => $transport,
+if ($digest_per_branch) {
+ while (my ($branchcode, $digests) = each %$upcoming_digest) {
+ send_digests({
+ sth => $sth_digest,
+ digests => $digests,
+ letter_code => 'PREDUEDGST',
+ message_name => 'advance_notice',
+ branchcode => $branchcode,
+ get_item_info => sub {
+ my $params = shift;
+ $params->{sth}->execute($params->{borrowernumber},
+ $params->{borrower_preferences}->{'days_in_advance'});
+ return sub {
+ $params->{sth}->fetchrow_hashref;
+ };
}
- )
- or warn "no letter of type '$letter_type' found. Please see sample_notices.sql";
- push @letters, $letter if $letter;
- }
-
- if ( @letters ) {
- if ($nomail) {
- for my $letter ( @letters ) {
- local $, = "\f";
- print $letter->{'content'};
- }
- }
- else {
- for my $letter ( @letters ) {
- C4::Letters::EnqueueLetter( { letter => $letter,
- borrowernumber => $borrowernumber,
- from_address => $from_address,
- message_transport_type => $letter->{message_transport_type} } );
- }
- }
+ });
}
-}
-# Now, run through all the people that want digests and send them
-PATRON: while ( my ( $borrowernumber, $digest ) = each %$due_digest ) {
- @letters = ();
- my $count = $digest->{count};
- my $from_address = $digest->{email};
-
- my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber,
- message_name => 'item_due' } );
- next PATRON unless $borrower_preferences; # how could this happen?
-
- my $letter_type = 'DUEDGST';
- $sth->execute($borrowernumber,'0');
- my $titles = "";
- while ( my $item_info = $sth->fetchrow_hashref()) {
- my @item_info = map { $_ =~ /^date|date$/ ? format_date($item_info->{$_}) : $item_info->{$_} || '' } @item_content_fields;
- $titles .= join("\t",@item_info) . "\n";
- }
-
- ## Get branch info for borrowers home library.
- my %branch_info = get_branch_info( $borrowernumber );
-
- for my $transport ( keys %{ $borrower_preferences->{'transports'} } ) {
- my $letter = parse_letter(
- {
- letter_code => $letter_type,
- borrowernumber => $borrowernumber,
- substitute => {
- count => $count,
- 'items.content' => $titles,
- %branch_info,
- },
- branchcode => $branch_info{"branches.branchcode"},
- message_transport_type => $transport,
+ while (my ($branchcode, $digests) = each %$due_digest) {
+ send_digests({
+ sth => $sth_digest,
+ digests => $due_digest,
+ letter_code => 'DUEDGST',
+ branchcode => $branchcode,
+ message_name => 'item_due',
+ get_item_info => sub {
+ my $params = shift;
+ $params->{sth}->execute($params->{borrowernumber}, 0);
+ return sub {
+ $params->{sth}->fetchrow_hashref;
+ };
}
- )
- or warn "no letter of type '$letter_type' found. Please see sample_notices.sql";
- push @letters, $letter if $letter;
+ });
}
-
- if ( @letters ) {
- if ($nomail) {
- for my $letter ( @letters ) {
- local $, = "\f";
- print $letter->{'content'};
+} else {
+ send_digests({
+ sth => $sth_digest,
+ digests => $upcoming_digest,
+ letter_code => 'PREDUEDGST',
+ message_name => 'advance_notice',
+ get_item_info => sub {
+ my $params = shift;
+ $params->{sth}->execute($params->{borrowernumber},
+ $params->{borrower_preferences}->{'days_in_advance'});
+ return sub {
+ $params->{sth}->fetchrow_hashref;
+ };
}
- }
- else {
- for my $letter ( @letters ) {
- C4::Letters::EnqueueLetter( { letter => $letter,
- borrowernumber => $borrowernumber,
- from_address => $from_address,
- message_transport_type => $letter->{message_transport_type} } );
+ });
+
+ send_digests({
+ sth => $sth_digest,
+ digests => $due_digest,
+ letter_code => 'DUEDGST',
+ message_name => 'item_due',
+ get_item_info => sub {
+ my $params = shift;
+ $params->{sth}->execute($params->{borrowernumber}, 0);
+ return sub {
+ $params->{sth}->fetchrow_hashref;
+ };
}
- }
- }
-
+ });
}
=head1 METHODS
sub parse_letter {
my $params = shift;
+
foreach my $required ( qw( letter_code borrowernumber ) ) {
return unless exists $params->{$required};
}
+ my $patron = Koha::Patrons->find( $params->{borrowernumber} );
my %table_params = ( 'borrowers' => $params->{'borrowernumber'} );
module => 'circulation',
letter_code => $params->{'letter_code'},
branchcode => $table_params{'branches'},
+ lang => $patron->lang,
substitute => $params->{'substitute'},
tables => \%table_params,
+ ( $params->{itemnumbers} ? ( loops => { items => $params->{itemnumbers} } ) : () ),
message_transport_type => $params->{message_transport_type},
);
}
-sub format_date {
- my $date_string = shift;
- my $dt=dt_from_string($date_string);
- return output_pref($dt);
-}
-
=head2 get_branch_info
=cut
my ( $borrowernumber ) = @_;
## Get branch info for borrowers home library.
- my $borrower_details = C4::Members::GetMember( borrowernumber => $borrowernumber );
- my $borrower_branchcode = $borrower_details->{'branchcode'};
- my $branch = C4::Branch::GetBranchDetail( $borrower_branchcode );
+ my $patron = Koha::Patrons->find( $borrowernumber );
+ my $branch = $patron->library->unblessed;
my %branch_info;
foreach my $key( keys %$branch ) {
$branch_info{"branches.$key"} = $branch->{$key};
return %branch_info;
}
+=head2 send_digests
+
+ send_digests({
+ digests => ...,
+ sth => ...,
+ letter_code => ...,
+ get_item_info => ...,
+ })
+
+Enqueue digested letters (or print them if -n was passed at command line).
+
+Parameters:
+
+=over 4
+
+=item C<$digests>
+
+Reference to the array of digested messages.
+
+=item C<$sth>
+
+Prepared statement handle for fetching overdue issues.
+
+=item C<$letter_code>
+
+String that denote the letter code.
+
+=item C<$get_item_info>
+
+Subroutine for executing prepared statement. Takes parameters $sth,
+$borrowernumber and $borrower_parameters and return a generator
+function that produce the matching rows.
+
+=back
+
+=cut
+
+sub send_digests {
+ my $params = shift;
+
+ PATRON: while ( my ( $borrowernumber, $digest ) = each %{$params->{digests}} ) {
+ @letters = ();
+ my $count = $digest->{count};
+ my $from_address = $digest->{email};
+
+ my %branch_info;
+ my $branchcode;
+
+ if (defined($params->{branchcode})) {
+ %branch_info = ();
+ $branchcode = $params->{branchcode};
+ } else {
+ ## Get branch info for borrowers home library.
+ %branch_info = get_branch_info( $borrowernumber );
+ $branchcode = $branch_info{'branches.branchcode'};
+ }
+
+ my $borrower_preferences =
+ C4::Members::Messaging::GetMessagingPreferences(
+ {
+ borrowernumber => $borrowernumber,
+ message_name => $params->{message_name}
+ }
+ );
+
+ next PATRON unless $borrower_preferences; # how could this happen?
+
+ my $next_item_info = $params->{get_item_info}->({
+ sth => $params->{sth},
+ borrowernumber => $borrowernumber,
+ borrower_preferences => $borrower_preferences
+ });
+ my $titles = "";
+ my @itemnumbers;
+ while ( my $item_info = $next_item_info->()) {
+ push @itemnumbers, $item_info->{itemnumber};
+ $titles .= C4::Letters::get_item_content( { item => $item_info, item_content_fields => \@item_content_fields } );
+ }
+
+ foreach my $transport ( keys %{ $borrower_preferences->{'transports'} } ) {
+ next if $transport eq 'itiva';
+ my $letter = parse_letter(
+ {
+ letter_code => $params->{letter_code},
+ borrowernumber => $borrowernumber,
+ substitute => {
+ count => $count,
+ 'items.content' => $titles,
+ %branch_info
+ },
+ itemnumbers => \@itemnumbers,
+ branchcode => $branchcode,
+ message_transport_type => $transport
+ }
+ );
+ unless ( $letter ){
+ warn "no letter of type '$params->{letter_type}' found for borrowernumber $borrowernumber. Please see sample_notices.sql";
+ next;
+ }
+ push @letters, $letter if $letter;
+ }
+
+ if ( @letters ) {
+ if ($nomail) {
+ for my $letter ( @letters ) {
+ local $, = "\f";
+ print $letter->{'content'};
+ }
+ }
+ else {
+ for my $letter ( @letters ) {
+ C4::Letters::EnqueueLetter( { letter => $letter,
+ borrowernumber => $borrowernumber,
+ from_address => $from_address,
+ message_transport_type => $letter->{message_transport_type} } );
+ }
+ }
+ }
+ }
+}
+
+cronlogaction({ action => 'End', info => "COMPLETED" });
+
1;
__END__