]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/support-scripts/generate_circ_notices.pl
added logic for generating a template from the combined overdue data (e.g. xml file...
[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) = 
41     ('/openils/conf/opensrf_core.xml', 0, 0, 0); 
42
43 GetOptions(
44     'osrf_osrf_config=s' => \$osrf_config,
45     'send-emails' => \$send_email,
46     'generate-day-intervals' => \$gen_day_intervals,
47     'days-back=s' => \$days_back,
48 );
49
50 sub help {
51     print <<HELP;
52         --config <config_file>
53         
54         --send-emails If set, generate email notices
55
56         --generate-day-intervals If set, notices which have a notify_interval of >= 1 day will be processed.
57
58         --days-back <days_back_comma_separted>  This is used to set the effective run date of the script.
59             This is useful if you don't want to generate notices on certain days.  For example, if you don't 
60             generate notices on the weekend, you would run this script on weekdays and set --days-back to 
61             0,1,2 when it's run on Monday to capture any notices from Saturday and Sunday. 
62 HELP
63 }
64
65
66 sub main {
67     osrf_connect($osrf_config);
68     $settings = OpenSRF::Utils::SettingsClient->new;
69
70     my $smtp_server = $settings->config_value(notifications => 'smtp_server');
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();
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() . "\n";
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({
197         ABSOLUTE => 1,
198     });
199
200     my $sender = $settings->config_value(
201         notifications => $type => 'sender_address') || 
202         $settings->config_value(notifications => 'sender_address');
203
204     my $context = {   
205         circ_list => $circ_list,
206         get_bib_attr => \&get_bib_attr,
207         parse_due_date => \&parse_due_date, # let the templates decide date format
208         smtp_sender => $sender,
209         smtp_repley => $sender, # XXX
210         notice => $notice,
211     };
212
213     push(@global_overdue_circs, $context) if 
214         $type eq 'overdue' and $notice->{file_append} =~ /always/i;
215
216     if($send_email and $circ_list->[0]->usr->email) {
217         if(my $tmpl = $notice->{email_template}) {
218             $tt->process($tmpl, $context, \&email_template_output)
219                 or $logger->error('notice: Template error '.$tt->error);
220         } 
221     } else {
222         push(@global_overdue_circs, $context) 
223             if $type eq 'overdue' and $notice->{file_append} =~ /noemail/i;
224     }
225 }
226
227 sub get_bib_attr {
228     my $circ = shift;
229     my $attr = shift;
230     my $copy = $circ->target_copy;
231     if($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) {
232         return $copy->dummy_title || '' if $attr eq 'title';
233         return $copy->dummy_author || '' if $attr eq 'author';
234     } else {
235         my $mvr = $U->record_to_mvr($copy->call_number->record);
236         return $mvr->title || '' if $attr eq 'title';
237         return $mvr->author || '' if $attr eq 'author';
238     }
239 }
240
241 # provides a date that Template::Plugin::Date can parse
242 sub parse_due_date {
243     my $circ = shift;
244     my $due = DateTime::Format::ISO8601->new->parse_datetime(clense_ISO8601($circ->due_date));
245     return sprintf(
246         "%0.2d:%0.2d:%0.2d %0.2d-%0.2d-%0.2d",
247         $due->hour,
248         $due->minute,
249         $due->second,
250         $due->day,
251         $due->month,
252         $due->year
253     );
254 }
255
256 sub escape_xml {
257     my $str = shift;
258     $str =~ s/&/&amp;/sog;
259     $str =~ s/</&lt;/sog;
260     $str =~ s/>/&gt;/sog;
261     return $str;
262 }
263
264
265 sub email_template_output {
266     my $str = shift;
267     print "$str\n";
268 }
269
270 sub fetch_circ_data {
271     my @circs = @_;
272
273         my $circ_lib_id = $circs[0]->circ_lib;
274         my $usr_id = $circs[0]->usr;
275         $logger->debug("notice: printing user:$usr_id circ_lib:$circ_lib_id");
276
277     my $usr = $e->retrieve_actor_user([
278         $usr_id,
279         {   flesh => 1,
280             flesh_fields => {
281                 au => [qw/card billing_address mailing_address/] 
282             }
283         }
284     ]);
285
286     my $circ_lib = $ORG_CACHE{$circ_lib_id} ||
287         $e->retrieve_actor_org_unit([
288             $circ_lib_id,
289             {   flesh => 1,
290                 flesh_fields => {
291                     aou => [qw/billing_address mailing_address/],
292                 }
293             }
294         ]);
295     $ORG_CACHE{$circ_lib_id} = $circ_lib;
296
297     my $circ_objs = $e->search_action_circulation([
298         {id => [map {$_->id} @circs]},
299         {   flesh => 3,
300             flesh_fields => {
301                 circ => [q/target_copy/],
302                 acp => ['call_number'],
303                 acn => ['record'],
304             }
305         }
306     ]);
307
308     $_->circ_lib($circ_lib) for @$circ_objs;
309     $_->usr($usr) for @$circ_objs;
310
311     return $circ_objs
312 }
313
314
315 sub make_date_range {
316         my $offset = shift;
317     #my $is_day_precision = shift; # window?
318
319         my $epoch = CORE::time + $offset;
320         my $date = DateTime->from_epoch(epoch => $epoch, time_zone => 'local');
321
322         $date->set_hour(0);
323         $date->set_minute(0);
324         $date->set_second(0);
325         my $start = "$date";
326         
327         $date->set_hour(23);
328         $date->set_minute(59);
329         $date->set_second(59);
330
331         return ($start, "$date");
332 }
333
334 main();