]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/support-scripts/generate_circ_notices.pl
1b78dd7f5a1105290e75869e9f7e57f82ec8011b
[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 my ($osrf_config, $send_email, $gen_day_intervals, $days_back, $gen_global_templates) = 
41     ('/openils/conf/opensrf_core.xml', 0, 0, 0, 0); 
42
43 GetOptions(
44     'osrf_osrf_config=s' => \$osrf_config,
45     'send-email' => \$send_email,
46     'generate-day-intervals' => \$gen_day_intervals,
47     'generate-global-templates' => \$gen_global_templates,
48     'days-back=s' => \$days_back,
49 );
50
51 sub help {
52     print <<HELP;
53         --config <config_file>
54         
55         --send-emails If set, generate email notices
56
57         --generate-day-intervals If set, notices which have a notify_interval of >= 1 day will be processed.
58
59         --days-back <days_back_comma_separted>  This is used to set the effective run date of the script.
60             This is useful if you don't want to generate notices on certain days.  For example, if you don't 
61             generate notices on the weekend, you would run this script on weekdays and set --days-back to 
62             0,1,2 when it's run on Monday to capture any notices from Saturday and Sunday. 
63 HELP
64 }
65
66
67 sub main {
68     osrf_connect($osrf_config);
69     $settings = OpenSRF::Utils::SettingsClient->new;
70
71     my $sender_address = $settings->config_value(notifications => 'sender_address');
72     my $od_sender_addr = $settings->config_value(notifications => overdue => 'sender_address') || $sender_address;
73     my $pd_sender_addr = $settings->config_value(notifications => predue => 'sender_address') || $sender_address;
74     my $overdue_notices = $settings->config_value(notifications => overdue => 'notice');
75     my $predue_notices = $settings->config_value(notifications => predue => 'notice');
76
77     $overdue_notices = [$overdue_notices] unless ref $overdue_notices eq 'ARRAY'; 
78     $predue_notices = [$predue_notices] unless ref $predue_notices eq 'ARRAY'; 
79
80     my @overdues = sort { 
81         OpenSRF::Utils->interval_to_seconds($a->{notify_interval}) <=> 
82         OpenSRF::Utils->interval_to_seconds($b->{notify_interval}) } @$overdue_notices;
83
84     my @predues = sort { 
85         OpenSRF::Utils->interval_to_seconds($a->{notify_interval}) <=> 
86         OpenSRF::Utils->interval_to_seconds($b->{notify_interval}) } @$predue_notices;
87
88     generate_notice_set($_, 'overdue') for @overdues;
89     generate_notice_set($_, 'predue') for @predues;
90
91     generate_global_overdue_file() if $gen_global_templates;
92 }
93
94 sub generate_global_overdue_file {
95     $logger->info("notice: processing ".scalar(@global_overdue_circs)." for global template");
96     return unless @global_overdue_circs;
97
98     my $tt = Template->new({ABSOLUTE => 1});
99
100     $tt->process(
101         $settings->config_value(notifications => overdue => 'combined_template'),
102         {
103             overdues => \@global_overdue_circs,
104             get_bib_attr => \&get_bib_attr,
105             parse_due_date => \&parse_due_date, # let the templates decide date format
106             escape_xml => \&escape_xml,
107         }, 
108         \&global_overdue_output
109     ) or $logger->error('notice: Template error '.$tt->error);
110 }
111
112 sub global_overdue_output {
113     print shift();
114 }
115
116
117 sub generate_notice_set {
118     my($notice, $type) = @_;
119
120     my $notify_interval = OpenSRF::Utils->interval_to_seconds($notice->{notify_interval});
121     $notify_interval = -$notify_interval if $type eq 'overdue';
122
123     my ($start_date, $end_date) = make_date_range(-$days_back + $notify_interval);
124
125     $logger->info("notice: retrieving circs with due date in range $start_date -> $end_date");
126
127     my $QUERY = {
128         select => {
129             circ => ['id']
130         }, 
131         from => 'circ', 
132         where => {
133             '+circ' => {
134                 checkin_time => undef, 
135                 '-or' => [
136                     {stop_fines => ["LOST","LONGOVERDUE","CLAIMSRETURNED"]},
137                     {stop_fines => undef}
138                 ],
139                                 due_date => {between => [$start_date, $end_date]},
140             }
141         }
142     };
143
144     # if a circ duration is defined for this type of notice
145     if(my $durs = $notice->{circ_duration_range}) {
146         $QUERY->{where}->{'+circ'}->{duration} = {between => [$durs->{from}, $durs->{to}]};
147     }
148
149     my $circs = $e->json_query($QUERY, {timeout => 18000, substream => 1});
150     process_circs($notice, $type, map {$_->{id}} @$circs);
151 }
152
153
154 sub process_circs {
155     my $notice = shift;
156     my $type = shift;
157     my @circs = @_;
158
159         return unless @circs;
160
161         $logger->info("notice: processing $type notices with notify interval ". 
162         $notice->{notify_interval}."  and ".scalar(@circs)." circs");
163
164         my $org; 
165         my $patron;
166         my @current;
167
168         my $x = 0;
169         for my $circ (@circs) {
170                 $circ = $e->retrieve_action_circulation($circ);
171
172                 if( !defined $org or 
173                                 $circ->circ_lib != $org  or $circ->usr ne $patron ) {
174                         $org = $circ->circ_lib;
175                         $patron = $circ->usr;
176                         generate_notice($notice, $type, @current) if @current;
177                         @current = ();
178                 }
179
180                 push(@current, $circ);
181                 $x++;
182         }
183
184         $logger->info("notice: processed $x circs");
185         generate_notice($notice, $type, @current);
186 }
187
188 my %ORG_CACHE;
189
190 sub generate_notice {
191     my $notice = shift;
192     my $type = shift;
193     my @circs = @_;
194     return unless @circs;
195     my $circ_list = fetch_circ_data(@circs);
196     my $tt = Template->new({ABSOLUTE => 1});
197
198     my $sender = $settings->config_value(
199         notifications => $type => 'sender_address') || 
200         $settings->config_value(notifications => 'sender_address');
201
202     my $context = {   
203         circ_list => $circ_list,
204         get_bib_attr => \&get_bib_attr,
205         parse_due_date => \&parse_due_date, # let the templates decide date format
206         smtp_sender => $sender,
207         smtp_repley => $sender, # XXX
208         notice => $notice,
209     };
210
211     push(@global_overdue_circs, $context) if 
212         $type eq 'overdue' and $notice->{file_append} =~ /always/i;
213
214     if($send_email and $notice->{email_notify} and 
215             my $email = $circ_list->[0]->usr->email) {
216
217         if(my $tmpl = $notice->{email_template}) {
218             $tt->process($tmpl, $context, 
219                 sub { 
220                     email_template_output($notice, $type, $context, $email, @_); 
221                 }
222             ) or $logger->error('notice: Template error '.$tt->error);
223         } 
224     } else {
225         push(@global_overdue_circs, $context) 
226             if $type eq 'overdue' and $notice->{file_append} =~ /noemail/i;
227     }
228 }
229
230 sub get_bib_attr {
231     my $circ = shift;
232     my $attr = shift;
233     my $copy = $circ->target_copy;
234     if($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) {
235         return $copy->dummy_title || '' if $attr eq 'title';
236         return $copy->dummy_author || '' if $attr eq 'author';
237     } else {
238         my $mvr = $U->record_to_mvr($copy->call_number->record);
239         return $mvr->title || '' if $attr eq 'title';
240         return $mvr->author || '' if $attr eq 'author';
241     }
242 }
243
244 # provides a date that Template::Plugin::Date can parse
245 sub parse_due_date {
246     my $circ = shift;
247     my $due = DateTime::Format::ISO8601->new->parse_datetime(clense_ISO8601($circ->due_date));
248     return sprintf(
249         "%0.2d:%0.2d:%0.2d %0.2d-%0.2d-%0.4d",
250         $due->hour,
251         $due->minute,
252         $due->second,
253         $due->day,
254         $due->month,
255         $due->year
256     );
257 }
258
259 sub escape_xml {
260     my $str = shift;
261     $str =~ s/&/&amp;/sog;
262     $str =~ s/</&lt;/sog;
263     $str =~ s/>/&gt;/sog;
264     return $str;
265 }
266
267
268 sub email_template_output {
269     my $notice = shift;
270     my $type = shift;
271     my $context = shift;
272     my $email = shift;
273     my $msg = shift;
274
275         my $sender = Email::Send->new({mailer => 'SMTP'});
276     my $smtp_server = $settings->config_value(notifications => 'smtp_server');
277     $logger->debug("notice: smtp server is $smtp_server");
278         $sender->mailer_args([Host => $smtp_server]);
279         my $stat = $sender->send($msg);
280
281         if( $stat and $stat->type eq 'success' ) {
282                 $logger->info("notice: successfully sent $type email to $email");
283         } else {
284                 $logger->warn("notice: unable to send $type email to $email: ".Dumper($stat));
285         # if we were unable to send the email, add this notice set to the global notify set
286         push(@global_overdue_circs, $context) 
287             if $type eq 'overdue' and $notice->{file_append} =~ /noemail/i;
288         }
289 }
290
291 sub fetch_circ_data {
292     my @circs = @_;
293
294         my $circ_lib_id = $circs[0]->circ_lib;
295         my $usr_id = $circs[0]->usr;
296         $logger->debug("notice: printing user:$usr_id circ_lib:$circ_lib_id");
297
298     my $usr = $e->retrieve_actor_user([
299         $usr_id,
300         {   flesh => 1,
301             flesh_fields => {
302                 au => [qw/card billing_address mailing_address/] 
303             }
304         }
305     ]);
306
307     my $circ_lib = $ORG_CACHE{$circ_lib_id} ||
308         $e->retrieve_actor_org_unit([
309             $circ_lib_id,
310             {   flesh => 1,
311                 flesh_fields => {
312                     aou => [qw/billing_address mailing_address/],
313                 }
314             }
315         ]);
316     $ORG_CACHE{$circ_lib_id} = $circ_lib;
317
318     my $circ_objs = $e->search_action_circulation([
319         {id => [map {$_->id} @circs]},
320         {   flesh => 3,
321             flesh_fields => {
322                 circ => [q/target_copy/],
323                 acp => ['call_number'],
324                 acn => ['record'],
325             }
326         }
327     ]);
328
329     $_->circ_lib($circ_lib) for @$circ_objs;
330     $_->usr($usr) for @$circ_objs;
331
332     return $circ_objs
333 }
334
335
336 sub make_date_range {
337         my $offset = shift;
338     #my $is_day_precision = shift; # window?
339
340         my $epoch = CORE::time + $offset;
341         my $date = DateTime->from_epoch(epoch => $epoch, time_zone => 'local');
342
343         $date->set_hour(0);
344         $date->set_minute(0);
345         $date->set_second(0);
346         my $start = "$date";
347         
348         $date->set_hour(23);
349         $date->set_minute(59);
350         $date->set_second(59);
351
352         return ($start, "$date");
353 }
354
355 main();