ea9d63e1d8a74b391da956a82b9c986a4525e5b8
[Evergreen.git] / Open-ILS / src / support-scripts / edi_pusher.pl
1 #!/usr/bin/perl
2 # ---------------------------------------------------------------
3 # Copyright (C) 2010 Equinox Software, Inc
4 # Author: Joe Atzberger <jatzberger@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
17 use strict;
18 use warnings;
19
20 use Data::Dumper;
21 use vars qw/$debug/;
22
23 use OpenILS::Utils::Cronscript;
24 use OpenILS::Utils::Fieldmapper;
25 use OpenILS::Application::AppUtils;
26 use OpenILS::Application::Acq::EDI;
27 use OpenSRF::Utils::Logger q/$logger/;
28
29 INIT {
30     $debug = 1;
31 }
32
33 my %defaults = (
34     'quiet' => 0,
35     'test'  => 0,   # TODO
36     'max-batch-size=i' => -1
37 );
38
39 my $cs = OpenILS::Utils::Cronscript->new(\%defaults);
40
41 my $opts = $cs->MyGetOptions();
42 my $e    = $cs->editor() or die "Failed to get new CStoreEditor";
43 my $hook = 'acqpo.activated';
44 my $defs = $e->search_action_trigger_event_definition({
45     hook    => $hook,
46     reactor => 'GeneratePurchaseOrderJEDI',
47     active  => 't'
48 });
49
50 $opts->{verbose} = 0 if $opts->{quiet};
51
52 print "FTP_PASSIVE is ", ($ENV{FTP_PASSIVE} ? "ON" : "OFF"),  "\n";
53
54 print "\nHook '$hook' is used in ", scalar(@$defs), " event definition(s):\n";
55
56 $Data::Dumper::Indent = 1;
57 my $remaining = $opts->{'max-batch-size'};
58
59 # FIXME: this is the disclusion subquery.  It discludes any PO that has
60 # a non-retry edi_message linked to it.  But that means that if there are
61 # mutliple EDI messages (say, some failed translation) and one marked retry,
62 # the PO is still discluded!  Perhaps there should never be multiple messages,
63 # but that makes testing much trickier (and is not DB-enforced).
64 #
65 # One approach might be to supplementally query for any "retry" messages that 
66 # are on active providers (and deduplicate).  
67
68 my $subq = {
69     select => { acqedim => ['purchase_order'] },
70     from   => 'acqedim',
71     where  => {
72         message_type   => 'ORDERS',
73         status         => {'!=' => 'retry' },
74         purchase_order => {'!=' => undef   }
75     }
76 };
77
78 foreach my $def (@$defs) {
79     last if $remaining == 0;
80     printf "%3s - '%s'\n", $def->id, $def->name;
81
82     # give me all completed JEDI events that link to purchase_orders 
83     # that have no delivery attempts or are in the retry state
84
85     my $query = {
86         select => {atev => ['id']},
87         from   => 'atev',
88         where  => {
89             event_def => $def->id,
90             state  => 'complete',
91             target => {
92                 'not in' => $subq
93             }
94         },
95         order_by => {atev => ['add_time']}
96     };
97
98     $query->{limit} = $remaining if $remaining > 0;
99
100     if ($opts->{verbose}) {
101         # $subq->{'select'}->{'acqedim'} = ['id', 'purchase_order', 'message_type', 'status'];
102         my $excluded = $e->json_query($subq);
103         print "Excluded: ", scalar(@$excluded), " purchase order(s):\n";
104         my $z = 0;
105         print map {sprintf "%7d%s", $_, (++$z % 5) ? '' : "\n"} sort {$a <=> $b} map {$_->{purchase_order}} @$excluded;
106         print "\n";
107     }
108
109     my $events = $e->json_query($query);
110
111     if(!$events) {
112         print STDERR   "error querying JEDI events for event definition ", $def->id, "\n";
113         $logger->error("error querying JEDI events for event definition ". $def->id);
114         next;
115     }
116
117     $remaining -= scalar(@$events);
118
119     print "Event definition ", $def->id, " has ", scalar(@$events), " (completed) event(s)\n";
120     foreach (@$events) {
121
122         my $event = $e->retrieve_action_trigger_event([
123             $_->{id}, 
124             {flesh => 1, flesh_fields => {atev => ['template_output']}}
125         ]);
126
127
128         my $target = $e->retrieve_acq_purchase_order([              # instead we retrieve it separately
129             $event->target, {
130                 flesh => 2,
131                 flesh_fields => {
132                     acqpo  => ['provider'],
133                     acqpro => ['edi_default'],
134                 },
135             }
136         ]);
137
138         # this may be a retry attempt.  if so, reuse the original edi_message
139         my $message = $e->search_acq_edi_message({
140             purchase_order => $target->id,
141             message_type => 'ORDERS', 
142             status => 'retry'
143         })->[0];
144
145         if(!$message) {
146             $message = Fieldmapper::acq::edi_message->new;
147             $message->create_time('NOW');   # will need this later when we try to update from the object
148             $message->purchase_order($target->id);
149             $message->message_type('ORDERS');
150             $message->isnew(1);
151         }
152
153         my $logstr = sprintf "provider %s (%s)", $target->provider->id, $target->provider->name;
154         unless ($target->provider->edi_default and $message->account($target->provider->edi_default->id)) {
155             printf STDERR "ERROR: No edi_default account found for $logstr.  File will not be sent!\n";
156         }
157
158         my $jedi = $event->template_output()->data;
159
160         # Crucial identifiers won't contain unicode characters, and EDIFACT
161         # (or at least our translator) generally can't handle them anyway.
162         $jedi =~ s/\\u[0-9a-f]{4}//g;
163
164         $message->jedi($jedi);
165
166         print "\ntarget->provider->edi_default->id: ", $target->provider->edi_default->id, "\n";
167         my $logstr2 = sprintf "event %s, PO %s, template_output %s", $_->{id}, $message->purchase_order, $event->template_output->id;
168         if ($opts->{test}) {
169             print "Test mode, skipping translation/send\n";
170             next;
171         }
172
173         printf "\nNow calling attempt_translation for $logstr2\n\n";
174
175         unless (OpenILS::Application::Acq::EDI->attempt_translation($message, 1)) {
176             print STDERR "ERROR: attempt_translation failed for $logstr2\n";
177             next;
178             # The premise here is that if the translator failed, it is better to try again later from a "fresh" fetched file
179             # than to add a cascade of failing inscrutable copies of the same message(s) to our DB.  
180         }
181
182         print "Writing new message + translation to DB for $logstr2\n";
183
184         $e->xact_begin;
185         if($message->isnew) {
186             unless($e->create_acq_edi_message($message)) {
187                 $logger->error("Error creating acq.edi_message for $logstr2: ".$e->die_event);
188                 next;
189             }
190         } else {
191             unless($e->update_acq_edi_message($message)) {
192                 $logger->error("Error updating acq.edi_message for $logstr2: ".$e->die_event);
193                 next;
194             }
195         }
196         $e->xact_commit;
197
198         print "Calling send_core(...) for message (", $message->id, ")\n";
199         my $res = OpenILS::Application::Acq::EDI->send_core($target->provider->edi_default, [$message->id]);
200         if (@$res) {
201             my $message_out = shift @$res;
202             print "\tmessage ", $message->id, " status: ", $message_out->status, "\n";
203         } else {
204             print STDERR "ERROR: send_core failed for message ", $message->id, "\n";
205         }
206     }
207 }
208
209 print "\ndone\n";
210
211 __END__
212
213 =head1 NAME
214
215 edi_pusher.pl - A script for generating and sending EDI files to remote accounts.
216
217 =head1 DESCRIPTION
218
219 This script is expected to be run via crontab, for the purpose of retrieving vendor EDI files.
220
221 =head1 OPTIONS
222
223   --max-batch-size=i  Limit the processing to a set number of events.
224
225 =head1 TODO
226
227 More docs here.
228
229 =head1 USAGE
230
231 B<FTP_PASSIVE=1> is recommended.  Depending on your vendors' and your own network environments, you may want to set/export
232 the environmental variable FTP_PASSIVE like:
233
234     export FTP_PASSIVE=1
235     # or
236     FTP_PASSIVE=1 Open-ILS/src/support-scripts/edi_pusher.pl
237
238 =head1 SEE ALSO
239
240     OpenILS::Utils::Cronscript
241     edi_fetcher.pl
242
243 =head1 AUTHOR
244
245 Joe Atzberger <jatzberger@esilibrary.com>
246
247 =cut