2 # ---------------------------------------------------------------
3 # Generates the overdue notices XML file
4 # ./eg_gen_overdue.pl <bootstap> 0
5 # generates today's notices
6 # ./eg_gen_overdue.pl <bootstap> 1 0
7 # generates notices for today - 1 and today
8 # ./eg_gen_overdue.pl <bootstap> 2 1 0
9 # ./eg_gen_overdue.pl <bootstap> 3 2 1 0 etc...
10 # ---------------------------------------------------------------
14 use strict; use warnings;
15 require '../../../Open-ILS/src/support-scripts/oils_header.pl';
16 use vars qw/$logger $apputils/;
18 use OpenILS::Const qw/:const/;
19 use OpenILS::Application::AppUtils;
22 use DateTime::Format::ISO8601;
23 use OpenSRF::Utils qw/:datetime/;
24 use OpenSRF::Utils::JSON;
25 use Unicode::Normalize;
26 use OpenILS::Const qw/:const/;
27 use OpenSRF::AppSession;
29 my $U = 'OpenILS::Application::AppUtils';
33 my $bsconfig = shift || die "usage: $0 <bootstrap_config>\n";
35 @goback = (0) unless @goback;
36 osrf_connect($bsconfig);
37 my $e = OpenILS::Utils::CStoreEditor->new;
39 my $smtp = $ENV{EG_OVERDUE_SMTP_HOST};
40 my $mail_sender = $ENV{EG_OVERDUE_EMAIL_SENDER};
42 # ---------------------------------------------------------------
43 # Set up the email template
44 my $etmpl = $ENV{EG_OVERDUE_EMAIL_TEMPLATE};
46 if( open(F,"$etmpl") ) {
48 $email_template = join('',@etmpl);
51 # ---------------------------------------------------------------
55 my @date = CORE::localtime;
60 my $mon = $date[4] + 1;
61 my $year = $date[5] + 1900;
68 <?xml version='1.0' encoding='UTF-8'?>
69 <file type="notice" date="$mon/$day/$year" time="$hour:$min:$sec">
73 print_notices($_) for @goback;
81 # -----------------------------------------------------------------------
82 # -----------------------------------------------------------------------
86 my $goback = shift || 0;
88 for my $day ( qw/ 7 14 30 / ) {
89 my ($start, $end) = make_date_range($day + $goback);
91 $logger->info("OD_notice: process date range $start -> $end");
95 checkin_time => undef,
96 due_date => { between => [ $start, $end ] },
98 { order_by => { circ => 'usr, circ_lib' } }
100 #my $circs = $e->search_action_circulation($query, {idlist=>1});
102 my $ses = OpenSRF::AppSession->create('open-ils.cstore');
103 my $req = $ses->request('open-ils.cstore.direct.action.circulation.id_list', @$query);
106 push(@$circs, $resp->content) while ($resp = $req->recv(timeout=>600));
108 process_circs( $circs, "${day}day" );
117 return unless @$circs;
119 $logger->info("OD_notice: processing range $range and ".scalar(@$circs)." potential circs");
126 for my $circ (@$circs) {
127 $circ = $e->retrieve_action_circulation($circ);
130 $circ->circ_lib != $org or $circ->usr ne $patron ) {
131 $org = $circ->circ_lib;
132 $patron = $circ->usr;
133 print_notice( $range, \@current ) if @current;
137 push( @current, $circ );
141 $logger->info("OD_notice: processed $x circs");
142 print_notice( $range, \@current );
145 sub make_date_range {
146 my $daysback = shift;
148 my $epoch = CORE::time - ($daysback * 24 * 60 * 60);
149 my $date = DateTime->from_epoch( epoch => $epoch, time_zone => 'local');
152 $date->set_minute(0);
153 $date->set_second(0);
157 $date->set_minute(59);
158 $date->set_second(59);
160 return ($start, "$date");
165 my( $range, $circs ) = @_;
166 return unless @$circs;
168 my $s1 = scalar(@$circs);
170 # we don't charge for lost or claimsreturned
174 $_->stop_fines ne OILS_STOP_FINES_LOST and
175 $_->stop_fines ne OILS_STOP_FINES_CLAIMSRETURNED
180 return unless @$circs;
182 my $s2 = $s1 - scalar(@$circs);
183 $logger->info("OD_notice: dropped $s2 lost/CR from processing...") if $s2;
185 my $org = $circs->[0]->circ_lib;
186 my $usr = $circs->[0]->usr;
187 $logger->debug("OD_notice: printing $range user:$usr org:$org");
189 my @patron_data = fetch_patron_data($usr);
190 my @org_data = fetch_org_data($org);
192 return unless (@patron_data and @org_data);
196 if( $email = $patron_data[0]->email
197 and $email =~ /.+\@.+/
198 and ($range eq '7day' or $range eq '14day') ) {
200 send_email($range, \@patron_data, \@org_data, $circs);
204 if( $patron_data[9] ) {
206 print "\t\t<notice type='overdue' count='$range'>\n";
207 print_patron_xml_chunk(@patron_data);
208 print_org_xml_chunk(@org_data);
209 print_circ_chunk($_) for @$circs;
210 print "\t\t</notice>\n";
213 # There is no zip, therefore no address.
214 $logger->warn("OD_notice: unable to send mail notification for $usr due to lack of valid address");
220 sub fetch_patron_data {
223 my $patron = $USER_CACHE{$user_id};
226 $logger->debug("OD_notice: fetching patron $user_id");
228 $patron = $e->retrieve_actor_user(
234 'au' => [qw/ card billing_address mailing_address /]
238 ) or return handle_event($e->event);
240 $USER_CACHE{$user_id} = $patron;
243 my $bc = $patron->card->barcode;
244 my $fn = $patron->first_given_name;
245 my $mn = $patron->second_given_name;
246 my $ln = $patron->family_name;
248 my ( $s1, $s2, $city, $state, $zip );
250 my $baddr = $patron->mailing_address;
251 unless( $baddr and $U->is_true($baddr->valid) ) {
252 $baddr = $patron->billing_address;
253 $baddr = undef unless( $baddr and $U->is_true($baddr->valid) );
257 $s1 = $baddr->street1;
258 $s2 = $baddr->street2;
259 $city = $baddr->city;
260 $state = $baddr->state;
261 $zip = $baddr->post_code;
264 $bc = entityize($bc);
265 $fn = entityize($fn);
266 $mn = entityize($mn);
267 $ln = entityize($ln);
268 $s1 = entityize($s1);
269 $s2 = entityize($s2);
270 $city = entityize($city);
271 $state = entityize($state);
272 $zip = entityize($zip);
274 return ( $patron, $bc, $fn, $mn, $ln, $s1, $s2, $city, $state, $zip );
278 sub print_patron_xml_chunk {
279 my( $patron, $bc, $fn, $mn, $ln, $s1, $s2, $city, $state, $zip ) = @_;
280 my $pid = $patron->id;
283 <id type="barcode">$bc</id>
284 <fullname>$fn $mn $ln</fullname>
285 <street1>$s1 $s2</street1>
286 <city_state_zip>$city, $state $zip</city_state_zip>
287 <sys_id>$pid</sys_id>
296 my $org = $ORG_CACHE{$org_id};
299 $logger->debug("OD_notice: fetching org $org_id");
301 $org = $e->retrieve_actor_org_unit(
307 { aou => [ qw/billing_address mailing_address/ ] }
310 ) or return handle_event($e->event);
312 $ORG_CACHE{$org_id} = $org;
315 my $name = $org->name;
316 my $phone = $org->phone;
317 my $email = $org->email;
320 my( $s1, $s2, $city, $state, $zip );
321 my $baddr = $org->billing_address || $org->mailing_address;
323 $s1 = $baddr->street1;
324 $s2 = $baddr->street2;
325 $city = $baddr->city;
326 $state = $baddr->state;
327 $zip = $baddr->post_code;
330 $name = entityize($name);
331 $phone = entityize($phone);
332 $s1 = entityize($s1);
333 $s2 = entityize($s2);
334 $city = entityize($city);
335 $state = entityize($state);
336 $zip = entityize($zip);
337 $email = entityize($email);
339 return ( $org, $name, $phone, $s1, $s2, $city, $state, $zip, $email );
343 sub print_org_xml_chunk {
344 my( $org, $name, $phone, $s1, $s2, $city, $state, $zip, $email ) = @_;
347 <libname>$name</libname>
348 <libphone>$phone</libphone>
349 <libstreet1>$s1 $s2</libstreet1>
350 <libcity_state_zip>$city, $state $zip</libcity_state_zip>
356 sub fetch_circ_data {
363 my $d = $circ->due_date;
364 $d =~ s/[T ].*//og; # just for logging
365 $logger->debug("OD_notice: processing circ ".$circ->id." $d");
367 my $due = DateTime::Format::ISO8601->new->parse_datetime(
368 clense_ISO8601($circ->due_date));
371 my $mon = $due->month;
372 my $year = $due->year;
374 my $copy = $e->retrieve_asset_copy($circ->target_copy)
375 or return handle_event($e->event);
377 my $bc = $copy->barcode;
379 if( $copy->call_number == OILS_PRECAT_CALL_NUMBER ) {
380 $title = $copy->dummy_title || "";
381 $author = $copy->dummy_author || "";
385 my $volume = $e->retrieve_asset_call_number(
391 acn => [ qw/record/ ]
395 ) or return handle_event($e->event);
397 $cn = $volume->label;
398 my $mods = $apputils->record_to_mvr($volume->record);
400 $title = $mods->title || "";
401 $author = $mods->author || "";
405 $title = entityize($title);
406 $author = entityize($author);
407 $cn = entityize($cn);
408 $bc = entityize($bc);
410 return( $title, $author, $cn, $bc, $day, $mon, $year );
414 sub print_circ_chunk {
416 my ( $title, $author, $cn, $bc, $day, $mon, $year ) = fetch_circ_data($circ);
420 <title>$title</title>
421 <author>$author</author>
422 <duedate>$mon/$day/$year</duedate>
424 <barcode>$bc</barcode>
425 <circ_id>$cid</circ_id>
433 my( $range, $patron_data, $org_data, $circs ) = @_;
434 my( $org, $org_name, $org_phone, $org_s1, $org_s2, $org_city, $org_state, $org_zip, $org_email ) = @$org_data;
435 my( $patron, $bc, $fn, $mn, $ln, $user_s1, $user_s2, $user_city, $user_state, $user_zip ) = @$patron_data;
437 return unless $SEND_EMAILS;
439 my $pemail = $patron_data->[0]->email;
441 my $tmpl = $email_template;
442 my @time = localtime;
443 my $year = $time[5] + 1900;
444 my $mon = $time[4] + 1;
447 my $r = ($range eq '7day') ? 7 : 14;
449 # - default to the global sender for the errors-to header
450 my $errors_to = $mail_sender;
452 # if they have an org setting for errors-to, use that as the errors-to address
453 if( my $set = $e->search_actor_org_unit_setting(
454 { name => 'org.bounced_emails', org_unit => $org->id } )->[0] ) {
456 my $bemail = OpenSRF::Utils::JSON->JSON2perl($set->value);
457 $errors_to = $bemail if $bemail;
461 $tmpl =~ s/\${EMAIL_RECIPIENT}/$pemail/;
462 $tmpl =~ s/\${EMAIL_SENDER}/$errors_to/o;
463 $tmpl =~ s/\${EMAIL_REPLY_TO}/$errors_to/;
464 $tmpl =~ s/\${EMAIL_ERRORS_TO}/$errors_to/;
465 $tmpl =~ s/\${EMAIL_HEADERS}//; # - we have no additional headers to add
467 $tmpl =~ s/\${RANGE}/$r/;
468 $tmpl =~ s/\${DATE}/$mon\/$day\/$year/;
469 $tmpl =~ s/\${FIRST_NAME}/$fn/;
470 $tmpl =~ s/\${MIDDLE_NAME}/$mn/;
471 $tmpl =~ s/\${LAST_NAME}/$ln/;
473 my ($itmpl) = $tmpl =~ /\${OVERDUE_ITEMS\[(.*)\]}/ms;
476 for my $circ (@$circs) {
477 my $circtmpl = $itmpl;
478 my ( $title, $author, $cn, $bc, $due_day, $due_mon, $due_year ) = fetch_circ_data($circ);
479 $circtmpl =~ s/\${TITLE}/$title/o;
480 $circtmpl =~ s/\${AUTHOR}/$author/o;
481 $circtmpl =~ s/\${CALL_NUMBER}/$cn/o;
482 $circtmpl =~ s/\${DUE_DAY}/$due_day/o;
483 $circtmpl =~ s/\${DUE_MONTH}/$due_mon/o;
484 $circtmpl =~ s/\${DUE_YEAR}/$due_year/o;
485 $circtmpl =~ s/\${ITEM_BARCODE}/$bc/o;
486 $items .= "$circtmpl\n";
489 $tmpl =~ s/\${OVERDUE_ITEMS\[.*\]}/$items/ms;
491 my $org_addr = "$org_s1 $org_s2 $org_city, $org_state $org_zip";
492 $tmpl =~ s/\${ORG_NAME}/$org_name/o;
493 $tmpl =~ s/\${ORG_ADDRESS}/$org_addr/o;
494 $tmpl =~ s/\${ORG_PHONE}/$org_phone/o;
496 $logger->debug("OD_notice: sending email to $pemail: $tmpl");
498 my $sender = Email::Send->new({mailer => 'SMTP'});
499 $sender->mailer_args([Host => $smtp]);
501 my $stat = $sender->send($tmpl);
503 if( $stat and $stat->type eq 'success' ) {
504 $logger->info("OD_notice: successfully sent overdue email");
506 $logger->warn("OD_notice: unable to send hold overdue email: ".Dumper($stat));
509 $logger->info("OD_notice: sending email to".$patron_data->[0]->email);
514 warn "OD_notice: ".Dumper($evt) . "\n";
515 $logger->error("OD_notice: ".Dumper($evt));
521 my $stuff = shift || return "";
522 $stuff =~ s/\</</og;
523 $stuff =~ s/\>/>/og;
524 $stuff =~ s/\&/&/og;
525 $stuff = NFC($stuff);
526 $stuff =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;