From 0438399769af9d9747a0185a8ae73a3cc944c00d Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Mon, 23 Jul 2012 10:44:41 -0400 Subject: [PATCH] ACQ order record fetcher and uploader script Some ACQ vendors support delivering MARC order record files directly from their order system to an ILS via FTP. (I've heard this called "one-click" ordering in the past). This commit includes a script to seek out such order record files and pass them on to the Acquisitions service for PO creation and potential activation. The script supports a number of options, configured in opensrf.xml, including which Vandelay (record import) options to use during record import/merge/overlay. See opensrf.xml.example for details. Example: ./acq_order_reader.pl \ --user admin \ --password demo123 \ -poll-interval 3 \ --debug & Signed-off-by: Bill Erickson Signed-off-by: Mike Rylander --- Open-ILS/examples/opensrf.xml.example | 48 +++ Open-ILS/src/support-scripts/acq_order_reader.pl | 381 +++++++++++++++++++++++ 2 files changed, 429 insertions(+) create mode 100755 Open-ILS/src/support-scripts/acq_order_reader.pl diff --git a/Open-ILS/examples/opensrf.xml.example b/Open-ILS/examples/opensrf.xml.example index ea6f18b397..b444cf32fc 100644 --- a/Open-ILS/examples/opensrf.xml.example +++ b/Open-ILS/examples/opensrf.xml.example @@ -271,6 +271,54 @@ vim:et:ts=4:sw=4: + + + + + /openils/var/data/acq_orders/ + + + ALL + + + + BR1 + BAB + CONS + CONS-BAB + false + + true + + + + + + + + + diff --git a/Open-ILS/src/support-scripts/acq_order_reader.pl b/Open-ILS/src/support-scripts/acq_order_reader.pl new file mode 100755 index 0000000000..ba0e2aa74c --- /dev/null +++ b/Open-ILS/src/support-scripts/acq_order_reader.pl @@ -0,0 +1,381 @@ +#!/usr/bin/perl +# Copyright (C) 2012 Equinox Software, Inc. +# Author: Bill Erickson +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +use strict; use warnings; +use MARC::Record; +use MARC::Batch; +use MARC::File::XML ( BinaryEncoding => 'UTF-8' ); +use MARC::File::USMARC; + +use Data::Dumper; +use File::Temp; +use Getopt::Long qw(:DEFAULT GetOptionsFromArray); +use Pod::Usage; +use File::Spec; + +use OpenSRF::Utils::Logger qw/$logger/; +use OpenSRF::AppSession; +use OpenSRF::EX qw/:try/; +use OpenSRF::Utils::SettingsClient; +use OpenSRF::Utils::Cache; +use OpenILS::Utils::Cronscript; +use OpenILS::Utils::CStoreEditor; +use OpenILS::Utils::Fieldmapper; +require 'oils_header.pl'; +use vars qw/$apputils/; + +my $acq_ses; +my $authtoken; +my $conf; +my $cache; +my $editor; +my $base_dir; +my $share_dir; +my $providers; +my $debug = 0; + +my %defaults = ( + 'osrf-config=s' => '/openils/conf/opensrf_core.xml', + 'user=s' => 'admin', + 'password=s' => '', + 'nodaemon' => 0, + 'poll-interval=i' => 10 +); + +# ----------------------------------------------------- +# Command-line args reading / munging +# ----------------------------------------------------- +$OpenILS::Utils::Cronscript::debug=1 if $debug; +$Getopt::Long::debug=1 if $debug > 1; +my $o = OpenILS::Utils::Cronscript->new(\%defaults); + +my @script_args = (); + +if (grep {$_ eq '--'} @ARGV) { + print "Splitting options into groups\n" if $debug; + while (@ARGV) { + $_ = shift @ARGV; + $_ eq '--' and last; # stop at the first -- + push @script_args, $_; + } +} else { + @script_args = @ARGV; + @ARGV = (); +} + +print "Calling MyGetOptions ", + (@script_args ? "with options: " . join(' ', @script_args) : 'without options from command line'), + "\n" if $debug; + +my $real_opts = $o->MyGetOptions(\@script_args); +$o->bootstrap; + +my $osrf_config = $real_opts->{'osrf-config'}; +my $oils_username = $real_opts->{user}; +my $oils_password = $real_opts->{password}; +my $help = $real_opts->{help}; +my $poll_interval = $real_opts->{'poll-interval'}; + $debug += $real_opts->{debug}; + +foreach (keys %$real_opts) { + print("real_opt->{$_} = ", $real_opts->{$_}, "\n") if $real_opts->{debug} or $debug; +} + +# FEEDBACK + +pod2usage(1) if $help; +unless ($oils_password) { + print STDERR "\nERROR: password option required for session login\n\n"; +} + +$debug and print Dumper($o); + +if ($debug) { + foreach my $ref (qw/osrf_config oils_username oils_password help debug/) { + no strict 'refs'; + printf "%16s => %s\n", $ref, (eval("\$$ref") || ''); + } +} + +$debug and print Dumper($real_opts); + +# ----------------------------------------------------- +# subs +# ----------------------------------------------------- + +# log in +sub new_auth_token { + $authtoken = oils_login($oils_username, $oils_password, 'staff') + or die "Unable to login to Evergreen as user $oils_username"; + return $authtoken; +} + +# log out +sub clear_auth_token { + $apputils->simplereq( + 'open-ils.auth', + 'open-ils.auth.session.delete', + $authtoken + ); +} + +sub push_file_to_acq { + my $file = shift; + my $args = shift; + + $logger->info("acq-or: pushing file '$file' to provider " . $args->{provider}); + + # Cache the file name like Vandelay does. ACQ will + # read contents of the cache and then delete them. + # The key can be any unique value. + my $key = $$ . time . rand(); + $cache->put_cache("vandelay_import_spool_$key", {path => $file}); + + # some arguments are not optional + $args->{create_po} = 1; + + # don't send our internal args to the service + my $local = delete $args->{_local}; + + my $req = $acq_ses->request( + 'open-ils.acq.process_upload_records', + $authtoken, + $key, + $args + ); + + while (my $resp = $req->recv(timeout => 600)) { + if(my $content = $resp->content) { + $debug and print Dumper($content); + } else { + warn "Request returned no data: " . Dumper($resp) . "\n"; + } + } + + # TODO: delete tmp queue? +} + +my %org_cache; +sub org_from_sn { + my $sn = shift; + return $org_cache{$sn} if $org_cache{$sn}; + my $org = $editor->search_actor_org_unit({shortname => $sn})->[0]; + if (!$org) { + warn "No such org unit in acq_order_reader config: '$sn'\n"; + return undef; + } + return $org_cache{$sn} = $org; +} + +# translate config info into a request arguments structure +sub args_from_provider_conf { + my $conf = shift; + my %args; + + my $pcode = $conf->{code}; + my $orgsn = $conf->{owner}; + + $debug and print "Extracting request args for provider $pcode at $orgsn\n"; + + my $org = org_from_sn($conf->{owner}) or return undef; + + my $provider = $editor->search_acq_provider({ + code => $pcode, + owner => $org->id + })->[0]; + + if (!$provider) { + warn "No such provider in acq_order_reader config: '$pcode'\n"; + return undef; + } + + my $oa = org_from_sn($conf->{ordering_agency}) or return undef; + + $args{provider} = $provider->id; + $args{ordering_agency} = $oa->id; + $args{activate_po} = ($conf->{activate_po} || '') =~ /true/i; + + # vandelay import options + my $vconf = $conf->{vandelay} || {}; + $args{vandelay} = {}; + + # value options + for my $opt ( + qw/ + match_quality_ratio + match_set + bib_source + merge_profile / ) { + + $args{vandelay}->{$opt} = $vconf->{$opt} + } + + # bool options + for my $opt ( + qw/ + create_assets + import_no_match + auto_overlay_exact + auto_overlay_1match + auto_overlay_best_match/ ) { + + $args{vandelay}->{$opt} = 1 if ($vconf->{$opt} || '') =~ /true/i; + } + + if ($vconf->{queue}) { + $args{vandelay}->{queue_name} = $vconf->{queue}; + $args{vandelay}->{existing_queue} = $vconf->{queue}; + + } else { + + # create a temporary queue + $args{vandelay}->{queue_name} = sprintf("acq-order-reader-%s-%s-%s", + $org->shortname, $provider->code, $apputils->epoch2ISO8601(time)); + } + + $args{_local} = { + provider_code => $pcode, # good for debugging + dirname => File::Spec->catfile($base_dir, $conf->{subdir}) + }; + + return \%args; +} + +# returns the list of new order record files that +# need to be processed for this vendor +sub check_provider_files { + my $args = shift; + my $dirname = $args->{_local}->{dirname}; + my $dh; + my @files; + + $debug and print "Searching for new files at $dirname\n"; + + if ( !opendir($dh, $dirname) ) { + warn "Couldn't open dir '$dirname': $!"; + return @files; + } + + @files = readdir $dh; + # ignore '.', '..', and hidden files + @files = grep {$_ !~ /^\./} @files; + + $logger->info("acq-or: found " . scalar(@files) . " ACQ order files at $dirname"); + + # return the file names w/ full path + return map {File::Spec->catfile($dirname, $_)} @files; +} + +# ----------------------------------------------------- +# Main script +# ----------------------------------------------------- + +osrf_connect($osrf_config); + +$conf = OpenSRF::Utils::SettingsClient->new; +$cache = OpenSRF::Utils::Cache->new; +$editor = OpenILS::Utils::CStoreEditor->new; +$acq_ses = OpenSRF::AppSession->create('open-ils.acq'); + +my $user = $editor->search_actor_user({usrname => $oils_username})->[0]; +if (!$user) { + warn "Invalid user: $oils_username\n"; + exit; +} + +# read configs +$base_dir = $conf->config_value(acq_order_reader => 'base_dir'); +$share_dir = $conf->config_value(acq_order_reader => 'shared_subdir'); +$providers = $conf->config_value(acq_order_reader => 'provider'); +$providers = [$providers] unless ref $providers eq 'ARRAY'; + +$debug and print Dumper($providers); + +# ----------------------------------------------------- +# main loop +# for each provider directory, plus the shared directory, check +# to see if there are any files pending. For any files found, push +# them up to the ACQ service, then delete the file +while (1) { + + new_auth_token(); + my $processed = 0; + + # explicit providers + for my $provider_conf (@$providers) { + my $args = args_from_provider_conf($provider_conf) or next; + my @files = check_provider_files($args); + push_file_to_acq($_, $args) for @files; + $processed += scalar(@files); + } + + # shared directory + # TODO + + clear_auth_token(); + + $logger->info("acq-or: loop processed $processed files"); + + $SIG{INT} = sub { + print "Cleaning up...\n"; + exit; # allows lockfile cleanup + }; + + # processing takes time. If we processed any records + # during the current iteration, immediately check again + # for more work. Otherwise, wait $poll_interval seconds + sleep $poll_interval if $processed == 0; +} + +__END__ + +=head1 NAME + +acq_order_reader.pl - Collect MARC order record files and pass them onto the ACQ service + +=head1 SYNOPSIS + +./acq_order_reader.pl [script opts ...] + +This script uses the EG common options from B. See --help output for those. + +Run C for full documentation. + +Note: this script has to be run in the same directory as B. + +Typical server-style execution will include a trailing C<&> to run in the background. + +=head1 OPTIONS + +The only required option is --password + + --password = + --user = default: admin + --nodaemon default: OFF When used with --spoolfile, turns off Net::Server mode and runs this utility in the foreground + + +=head2 Old style: --noqueue and associated options + +=head1 EXAMPLES + +./acq_order_reader.pl --user admin --password demo123 + +./acq_order_reader.pl --user admin --password demo123 -poll-interval 3 --debug --nodaemon + +=head1 AUTHORS + + Bill Erickson + Code liberally copied from marc_stream_importer.pl + +=cut -- 2.11.0