]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/support-scripts/generate_circ_notices.pl
implemented the ability to go back in time (aka days_back). made option variable...
[Evergreen.git] / Open-ILS / src / support-scripts / generate_circ_notices.pl
1 #!/usr/bin/perl
2 # ---------------------------------------------------------------
3 # Copyright (C) 2008  Georgia Public Library Service
4 # Bill Erickson <erickson@esilibrary.com>
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #  ---------------------------------------------------------------
16 use strict; use warnings;
17 require 'oils_header.pl';
18 use vars qw/$logger/;
19 use DateTime;
20 use Template;
21 use Data::Dumper;
22 use Email::Send;
23 use Getopt::Long;
24 use Unicode::Normalize;
25 use DateTime::Format::ISO8601;
26 use OpenSRF::Utils qw/:datetime/;
27 use OpenSRF::Utils::JSON;
28 use OpenSRF::Utils::SettingsClient;
29 use OpenSRF::AppSession;
30 use OpenILS::Const qw/:const/;
31 use OpenILS::Application::AppUtils;
32 use OpenILS::Const qw/:const/;
33 my $U = 'OpenILS::Application::AppUtils';
34
35 my $settings = undef;
36 my $e = OpenILS::Utils::CStoreEditor->new;
37
38 my @global_overdue_circs; # all circ collections stored here go into the final global XML file
39
40 # - set up default values
41 my $opt_osrf_config = '/openils/conf/opensrf_core.xml';
42 my $opt_send_email = 0;
43 my $opt_gen_day_intervals = 0;
44 my $opt_days_back = '';
45 my $opt_gen_global_templates = 0;
46 my $opt_show_help = 0;
47 my $opt_append_global_email_fail;
48
49 GetOptions(
50     'osrf_opt_osrf_config=s' => \$opt_osrf_config,
51     'send-email' => \$opt_send_email,
52     'generate-day-intervals' => \$opt_gen_day_intervals,
53     'generate-global-templates' => \$opt_gen_global_templates,
54     'days-back=s' => \$opt_days_back,
55     'append-global-email-fail' => \$opt_append_global_email_fail,
56     'help' => \$opt_show_help,
57 );
58
59 help() and exit if $opt_show_help;
60
61 sub help {
62     print <<HELP;
63
64 Evergreen Circulation Notice Generator
65
66     --config <config_file>
67     
68     --send-emails 
69         If set, generate email notices
70
71     --generate-day-intervals
72         If set, notices which have a notify_interval of >= 1 day will be processed.
73
74     --generate-global-templates
75         Collect all non-emailed notices into a global set and generate templates based on that set.
76
77     --append-global-email-fail
78         If an attempt was made to send an email notice but it failed, the notice is appended
79         to the global notice file set.  This will only have any bearing if --generate-global-templates
80         is enabled.
81
82     --days-back <days_back_comma_separted>  This is used to set the effective run date of the script.
83         This is useful if you don't want to generate notices on certain days.  For example, if you don't 
84         generate notices on the weekend, you would run this script on weekdays and set --days-back to 
85         0,1,2 when it's run on Monday to capture any notices from Saturday and Sunday. 
86
87     --help 
88         Print this help message
89 HELP
90 }
91
92
93 sub main {
94     osrf_connect($opt_osrf_config);
95     $settings = OpenSRF::Utils::SettingsClient->new;
96
97     my $sender_address = $settings->config_value(notifications => 'sender_address');
98     my $od_sender_addr = $settings->config_value(notifications => overdue => 'sender_address') || $sender_address;
99     my $pd_sender_addr = $settings->config_value(notifications => predue => 'sender_address') || $sender_address;
100     my $overdue_notices = $settings->config_value(notifications => overdue => 'notice');
101     my $predue_notices = $settings->config_value(notifications => predue => 'notice');
102
103     $overdue_notices = [$overdue_notices] unless ref $overdue_notices eq 'ARRAY'; 
104     $predue_notices = [$predue_notices] unless ref $predue_notices eq 'ARRAY'; 
105
106     my @overdues = sort { 
107         OpenSRF::Utils->interval_to_seconds($a->{notify_interval}) <=> 
108         OpenSRF::Utils->interval_to_seconds($b->{notify_interval}) } @$overdue_notices;
109
110     my @predues = sort { 
111         OpenSRF::Utils->interval_to_seconds($a->{notify_interval}) <=> 
112         OpenSRF::Utils->interval_to_seconds($b->{notify_interval}) } @$predue_notices;
113
114     for my $db (($opt_days_back) ? split(',', $opt_days_back) : 0) {
115         generate_notice_set($_, 'overdue', $db) for @overdues;
116         generate_notice_set($_, 'predue', $db) for @predues;
117     }
118
119     generate_global_overdue_file() if $opt_gen_global_templates;
120 }
121
122 sub generate_global_overdue_file {
123     $logger->info("notice: processing ".scalar(@global_overdue_circs)." for global template");
124     return unless @global_overdue_circs;
125
126     my $tt = Template->new({ABSOLUTE => 1});
127
128     $tt->process(
129         $settings->config_value(notifications => overdue => 'combined_template'),
130         {
131             overdues => \@global_overdue_circs,
132             get_bib_attr => \&get_bib_attr,
133             parse_due_date => \&parse_due_date, # let the templates decide date format
134             escape_xml => \&escape_xml,
135         }, 
136         \&global_overdue_output
137     ) or $logger->error('notice: Template error '.$tt->error);
138 }
139
140 sub global_overdue_output {
141     print shift();
142 }
143
144
145 sub generate_notice_set {
146     my($notice, $type, $days_back) = @_;
147
148     my $notify_interval = OpenSRF::Utils->interval_to_seconds($notice->{notify_interval});
149     $notify_interval = -$notify_interval if $type eq 'overdue';
150
151     my ($start_date, $end_date) = make_date_range($notify_interval - $days_back * 86400);
152
153     $logger->info("notice: retrieving circs with due date in range $start_date -> $end_date");
154
155     my $QUERY = {
156         select => {
157             circ => ['id']
158         }, 
159         from => 'circ', 
160         where => {
161             '+circ' => {
162                 checkin_time => undef, 
163                 '-or' => [
164                     {stop_fines => ["LOST","LONGOVERDUE","CLAIMSRETURNED"]},
165                     {stop_fines => undef}
166                 ],
167                                 due_date => {between => [$start_date, $end_date]},
168             }
169         }
170     };
171
172     # if a circ duration is defined for this type of notice
173     if(my $durs = $notice->{circ_duration_range}) {
174         $QUERY->{where}->{'+circ'}->{duration} = {between => [$durs->{from}, $durs->{to}]};
175     }
176
177     my $circs = $e->json_query($QUERY, {timeout => 18000, substream => 1});
178     process_circs($notice, $type, map {$_->{id}} @$circs);
179 }
180
181
182 sub process_circs {
183     my $notice = shift;
184     my $type = shift;
185     my @circs = @_;
186
187         return unless @circs;
188
189         $logger->info("notice: processing $type notices with notify interval ". 
190         $notice->{notify_interval}."  and ".scalar(@circs)." circs");
191
192         my $org; 
193         my $patron;
194         my @current;
195
196         my $x = 0;
197         for my $circ (@circs) {
198                 $circ = $e->retrieve_action_circulation($circ);
199
200                 if( !defined $org or 
201                                 $circ->circ_lib != $org  or $circ->usr ne $patron ) {
202                         $org = $circ->circ_lib;
203                         $patron = $circ->usr;
204                         generate_notice($notice, $type, @current) if @current;
205                         @current = ();
206                 }
207
208                 push(@current, $circ);
209                 $x++;
210         }
211
212         $logger->info("notice: processed $x circs");
213         generate_notice($notice, $type, @current);
214 }
215
216 my %ORG_CACHE;
217
218 sub generate_notice {
219     my $notice = shift;
220     my $type = shift;
221     my @circs = @_;
222     return unless @circs;
223     my $circ_list = fetch_circ_data(@circs);
224     my $tt = Template->new({ABSOLUTE => 1});
225
226     my $sender = $settings->config_value(
227         notifications => $type => 'sender_address') || 
228         $settings->config_value(notifications => 'sender_address');
229
230     my $context = {   
231         circ_list => $circ_list,
232         get_bib_attr => \&get_bib_attr,
233         parse_due_date => \&parse_due_date, # let the templates decide date format
234         smtp_sender => $sender,
235         smtp_repley => $sender, # XXX
236         notice => $notice,
237     };
238
239     push(@global_overdue_circs, $context) if 
240         $type eq 'overdue' and $notice->{file_append} =~ /always/i;
241
242     if($opt_send_email and $notice->{email_notify} and 
243             my $email = $circ_list->[0]->usr->email) {
244
245         if(my $tmpl = $notice->{email_template}) {
246             $tt->process($tmpl, $context, 
247                 sub { 
248                     email_template_output($notice, $type, $context, $email, @_); 
249                 }
250             ) or $logger->error('notice: Template error '.$tt->error);
251         } 
252     } else {
253         push(@global_overdue_circs, $context) 
254             if $type eq 'overdue' and $notice->{file_append} =~ /noemail/i;
255     }
256 }
257
258 sub get_bib_attr {
259     my $circ = shift;
260     my $attr = shift;
261     my $copy = $circ->target_copy;
262     if($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) {
263         return $copy->dummy_title || '' if $attr eq 'title';
264         return $copy->dummy_author || '' if $attr eq 'author';
265     } else {
266         my $mvr = $U->record_to_mvr($copy->call_number->record);
267         return $mvr->title || '' if $attr eq 'title';
268         return $mvr->author || '' if $attr eq 'author';
269     }
270 }
271
272 # provides a date that Template::Plugin::Date can parse
273 sub parse_due_date {
274     my $circ = shift;
275     my $due = DateTime::Format::ISO8601->new->parse_datetime(clense_ISO8601($circ->due_date));
276     return sprintf(
277         "%0.2d:%0.2d:%0.2d %0.2d-%0.2d-%0.4d",
278         $due->hour,
279         $due->minute,
280         $due->second,
281         $due->day,
282         $due->month,
283         $due->year
284     );
285 }
286
287 sub escape_xml {
288     my $str = shift;
289     $str =~ s/&/&amp;/sog;
290     $str =~ s/</&lt;/sog;
291     $str =~ s/>/&gt;/sog;
292     return $str;
293 }
294
295
296 sub email_template_output {
297     my $notice = shift;
298     my $type = shift;
299     my $context = shift;
300     my $email = shift;
301     my $msg = shift;
302
303         my $sender = Email::Send->new({mailer => 'SMTP'});
304     my $smtp_server = $settings->config_value(notifications => 'smtp_server');
305     $logger->debug("notice: smtp server is $smtp_server");
306         $sender->mailer_args([Host => $smtp_server]);
307         my $stat = $sender->send($msg);
308
309         if( $stat and $stat->type eq 'success' ) {
310                 $logger->info("notice: successfully sent $type email to $email");
311         } else {
312                 $logger->warn("notice: unable to send $type email to $email: ".Dumper($stat));
313         # if we were unable to send the email, add this notice set to the global notify set
314         push(@global_overdue_circs, $context) 
315             if $opt_append_global_email_fail and 
316                 $type eq 'overdue' and $notice->{file_append} =~ /noemail/i;
317         }
318 }
319
320 sub fetch_circ_data {
321     my @circs = @_;
322
323         my $circ_lib_id = $circs[0]->circ_lib;
324         my $usr_id = $circs[0]->usr;
325         $logger->debug("notice: printing user:$usr_id circ_lib:$circ_lib_id");
326
327     my $usr = $e->retrieve_actor_user([
328         $usr_id,
329         {   flesh => 1,
330             flesh_fields => {
331                 au => [qw/card billing_address mailing_address/] 
332             }
333         }
334     ]);
335
336     my $circ_lib = $ORG_CACHE{$circ_lib_id} ||
337         $e->retrieve_actor_org_unit([
338             $circ_lib_id,
339             {   flesh => 1,
340                 flesh_fields => {
341                     aou => [qw/billing_address mailing_address/],
342                 }
343             }
344         ]);
345     $ORG_CACHE{$circ_lib_id} = $circ_lib;
346
347     my $circ_objs = $e->search_action_circulation([
348         {id => [map {$_->id} @circs]},
349         {   flesh => 3,
350             flesh_fields => {
351                 circ => [q/target_copy/],
352                 acp => ['call_number'],
353                 acn => ['record'],
354             }
355         }
356     ]);
357
358     $_->circ_lib($circ_lib) for @$circ_objs;
359     $_->usr($usr) for @$circ_objs;
360
361     return $circ_objs
362 }
363
364
365 sub make_date_range {
366         my $offset = shift;
367     #my $is_day_precision = shift; # window?
368
369         my $epoch = CORE::time + $offset;
370         my $date = DateTime->from_epoch(epoch => $epoch, time_zone => 'local');
371
372         $date->set_hour(0);
373         $date->set_minute(0);
374         $date->set_second(0);
375         my $start = "$date";
376         
377         $date->set_hour(23);
378         $date->set_minute(59);
379         $date->set_second(59);
380
381         return ($start, "$date");
382 }
383
384 main();