]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/support-scripts/acq_order_reader.pl
Fix empty statuses filter
[working/Evergreen.git] / Open-ILS / src / support-scripts / acq_order_reader.pl
1 #!/usr/bin/perl
2 # Copyright (C) 2012 Equinox Software, Inc.
3 # Author: Bill Erickson <erickson@esilibrary.com>
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14
15 use strict; use warnings;
16 use MARC::Record;
17 use MARC::Batch;
18 use MARC::File::XML ( BinaryEncoding => 'UTF-8' );
19 use MARC::File::USMARC;
20
21 use Data::Dumper;
22 use File::Temp;
23 use Getopt::Long qw(:DEFAULT GetOptionsFromArray);
24 use Pod::Usage;
25 use File::Spec;
26
27 use OpenSRF::Utils::Logger qw/$logger/;
28 use OpenSRF::AppSession;
29 use OpenSRF::EX qw/:try/;
30 use OpenSRF::Utils::SettingsClient;
31 use OpenSRF::Utils::Cache;
32 use OpenILS::Utils::Cronscript;
33 use OpenILS::Utils::CStoreEditor;
34 use OpenILS::Utils::Fieldmapper;
35 require 'oils_header.pl';
36 use vars qw/$apputils/;
37
38 my $acq_ses;
39 my $authtoken;
40 my $conf;
41 my $cache;
42 my $editor;
43 my $base_dir;
44 my $share_dir;
45 my $providers;
46 my $debug = 0;
47
48 my %defaults = (
49     'osrf-config=s' => '/openils/conf/opensrf_core.xml',
50     'user=s'        => 'admin',
51     'password=s'    => '',
52     'nodaemon'      => 0,
53     'poll-interval=i' => 10
54 );
55
56 # -----------------------------------------------------
57 # Command-line args reading / munging
58 # -----------------------------------------------------
59 $OpenILS::Utils::Cronscript::debug=1 if $debug;
60 $Getopt::Long::debug=1 if $debug > 1;
61 my $o = OpenILS::Utils::Cronscript->new(\%defaults);
62
63 my @script_args = ();
64
65 if (grep {$_ eq '--'} @ARGV) {
66     print "Splitting options into groups\n" if $debug;
67     while (@ARGV) {
68         $_ = shift @ARGV;
69         $_ eq '--' and last;    # stop at the first --
70         push @script_args, $_;
71     }
72 } else {
73     @script_args = @ARGV;
74     @ARGV = ();
75 }
76
77 print "Calling MyGetOptions ",
78     (@script_args ? "with options: " . join(' ', @script_args) : 'without options from command line'),
79     "\n" if $debug;
80
81 my $real_opts = $o->MyGetOptions(\@script_args);
82 $o->bootstrap;
83
84 my $osrf_config   = $real_opts->{'osrf-config'};
85 my $oils_username = $real_opts->{user};
86 my $oils_password = $real_opts->{password};
87 my $help          = $real_opts->{help};
88 my $poll_interval = $real_opts->{'poll-interval'};
89    $debug         += $real_opts->{debug};
90
91 foreach (keys %$real_opts) {
92     print("real_opt->{$_} = ", $real_opts->{$_}, "\n") if $real_opts->{debug} or $debug;
93 }
94
95 # FEEDBACK
96
97 pod2usage(1) if $help;
98 unless ($oils_password) {
99     print STDERR "\nERROR: password option required for session login\n\n";
100 }
101
102 $debug and print Dumper($o);
103
104 if ($debug) {
105     foreach my $ref (qw/osrf_config oils_username oils_password help debug/) {
106         no strict 'refs';
107         printf "%16s => %s\n", $ref, (eval("\$$ref") || '');
108     }
109 }
110
111 $debug and print Dumper($real_opts);
112
113 # -----------------------------------------------------
114 # subs
115 # -----------------------------------------------------
116
117 # log in
118 sub new_auth_token {
119     $authtoken = oils_login($oils_username, $oils_password, 'staff') 
120         or die "Unable to login to Evergreen as user $oils_username";
121     return $authtoken;
122 }
123
124 # log out
125 sub clear_auth_token {
126     $apputils->simplereq(
127         'open-ils.auth',
128         'open-ils.auth.session.delete',
129         $authtoken
130     );
131 }
132
133 sub push_file_to_acq {
134     my $file = shift;
135     my $args = shift;
136
137     $logger->info("acq-or: pushing file '$file' to provider " . $args->{provider});
138
139     # Cache the file name like Vandelay does.  ACQ will 
140     # read contents of the cache and then delete them.
141     # The key can be any unique value.
142     my $key = $$ . time . rand();
143     $cache->put_cache("vandelay_import_spool_$key", {path => $file});
144
145     # some arguments are not optional
146     $args->{create_po} = 1;
147
148     # don't send our internal args to the service
149     my $local = delete $args->{_local};
150
151     my $req = $acq_ses->request(
152         'open-ils.acq.process_upload_records',
153         $authtoken,
154         $key, 
155         $args
156     );
157
158     while (my $resp = $req->recv(timeout => 600)) {
159         if(my $content = $resp->content) {
160             $debug and print Dumper($content);
161         } else {
162             warn "Request returned no data: " . Dumper($resp) . "\n";
163         }
164     }
165
166     # TODO: delete tmp queue?
167 }
168
169 my %org_cache;
170 sub org_from_sn {
171     my $sn = shift;
172     return $org_cache{$sn} if $org_cache{$sn};
173     my $org = $editor->search_actor_org_unit({shortname => $sn})->[0];
174     if (!$org) {
175         warn "No such org unit in acq_order_reader config: '$sn'\n";
176         return undef;
177     }
178     return $org_cache{$sn} = $org;
179 }
180
181 # translate config info into a request arguments structure
182 sub args_from_provider_conf {
183     my $conf = shift;
184     my %args;
185
186     my $pcode = $conf->{code};
187     my $orgsn = $conf->{owner};
188
189     $debug and print "Extracting request args for provider $pcode at $orgsn\n";
190
191     my $org = org_from_sn($conf->{owner}) or return undef;
192
193     my $provider = $editor->search_acq_provider({
194         code => $pcode,
195         owner => $org->id
196     })->[0];
197
198     if (!$provider) {
199         warn "No such provider in acq_order_reader config: '$pcode'\n";
200         return undef;
201     }
202
203     my $oa = org_from_sn($conf->{ordering_agency}) or return undef;
204
205     $args{provider} = $provider->id;
206     $args{ordering_agency} = $oa->id;
207     $args{activate_po} = ($conf->{activate_po} || '') =~ /true/i;
208     
209     # vandelay import options
210     my $vconf = $conf->{vandelay} || {};
211     $args{vandelay} = {};
212
213     # value options
214     for my $opt (
215         qw/
216             match_quality_ratio 
217             match_set 
218             bib_source 
219             merge_profile / ) {
220
221         $args{vandelay}->{$opt} = $vconf->{$opt} 
222     }
223
224     # bool options
225     for my $opt (
226         qw/
227             create_assets
228             import_no_match 
229             auto_overlay_exact 
230             auto_overlay_1match 
231             auto_overlay_best_match/ ) {
232
233         $args{vandelay}->{$opt} = 1 if ($vconf->{$opt} || '') =~ /true/i;
234     }
235
236     if ($vconf->{queue}) {
237         $args{vandelay}->{queue_name} = $vconf->{queue};
238         $args{vandelay}->{existing_queue} = $vconf->{queue};
239
240     } else {
241
242         # create a temporary queue
243         $args{vandelay}->{queue_name} = sprintf("acq-order-reader-%s-%s-%s", 
244             $org->shortname, $provider->code, $apputils->epoch2ISO8601(time));
245     }
246
247     $args{_local} = {
248         provider_code => $pcode, # good for debugging
249         dirname => File::Spec->catfile($base_dir, $conf->{subdir})
250     };
251
252     return \%args;
253 }
254
255 # returns the list of new order record files that
256 # need to be processed for this vendor
257 sub check_provider_files {
258     my $args = shift;
259     my $dirname = $args->{_local}->{dirname};
260     my $dh;
261     my @files;
262
263     $debug and print "Searching for new files at $dirname\n";
264
265     if ( !opendir($dh, $dirname) ) {
266         warn "Couldn't open dir '$dirname': $!";
267         return @files;
268     }
269
270     @files = readdir $dh;
271     # ignore '.', '..', and hidden files
272     @files = grep {$_ !~ /^\./} @files;
273
274     $logger->info("acq-or: found " . scalar(@files) . " ACQ order files at $dirname");
275
276     # return the file names w/ full path
277     return map {File::Spec->catfile($dirname, $_)} @files;
278 }
279
280 # -----------------------------------------------------
281 # Main script
282 # -----------------------------------------------------
283
284 osrf_connect($osrf_config);
285
286 $conf = OpenSRF::Utils::SettingsClient->new;
287 $cache = OpenSRF::Utils::Cache->new;
288 $editor = OpenILS::Utils::CStoreEditor->new;
289 $acq_ses = OpenSRF::AppSession->create('open-ils.acq');
290
291 my $user = $editor->search_actor_user({usrname => $oils_username})->[0];
292 if (!$user) {
293     warn "Invalid user: $oils_username\n";
294     exit;
295 }
296
297 # read configs
298 $base_dir = $conf->config_value(acq_order_reader => 'base_dir');
299 $share_dir = $conf->config_value(acq_order_reader => 'shared_subdir');
300 $providers = $conf->config_value(acq_order_reader => 'provider');
301 $providers = [$providers] unless ref $providers eq 'ARRAY';
302
303 $debug and print Dumper($providers);
304
305 # -----------------------------------------------------
306 # main loop
307 # for each provider directory, plus the shared directory, check
308 # to see if there are any files pending.  For any files found, push
309 # them up to the ACQ service, then delete the file
310 while (1) {
311
312     new_auth_token();
313     my $processed = 0;
314
315     # explicit providers
316     for my $provider_conf (@$providers) {
317         my $args = args_from_provider_conf($provider_conf) or next;
318         my @files = check_provider_files($args);
319         push_file_to_acq($_, $args) for @files;
320         $processed += scalar(@files);
321     }
322     
323     # shared directory
324     # TODO
325
326     clear_auth_token();
327
328     $logger->info("acq-or: loop processed $processed files");
329
330     $SIG{INT} = sub { 
331         print "Cleaning up...\n";
332         exit; # allows lockfile cleanup
333     };
334
335     # processing takes time.  If we processed any records
336     # during the current iteration, immediately check again
337     # for more work.  Otherwise, wait $poll_interval seconds
338     sleep $poll_interval if $processed == 0;
339 }
340
341 __END__
342
343 =head1 NAME
344
345 acq_order_reader.pl - Collect MARC order record files and pass them onto the ACQ service
346
347 =head1 SYNOPSIS
348
349 ./acq_order_reader.pl [script opts ...]
350
351 This script uses the EG common options from B<Cronscript>.  See --help output for those.
352
353 Run C<perldoc marc_stream_importer.pl> for full documentation.
354
355 Note: this script has to be run in the same directory as B<oils_header.pl>.
356
357 Typical server-style execution will include a trailing C<&> to run in the background.
358
359 =head1 OPTIONS
360
361 The only required option is --password
362
363  --password         =<eg_password>
364  --user             =<eg_username>  default: admin
365  --nodaemon                         default: OFF       When used with --spoolfile, turns off Net::Server mode and runs this utility in the foreground
366
367
368 =head2 Old style: --noqueue and associated options
369
370 =head1 EXAMPLES
371
372 ./acq_order_reader.pl --user admin --password demo123
373
374 ./acq_order_reader.pl --user admin --password demo123 -poll-interval 3 --debug --nodaemon
375
376 =head1 AUTHORS
377
378     Bill Erickson <erickson@esilibrary.com>
379     Code liberally copied from marc_stream_importer.pl
380
381 =cut