[open-ils-commits] [GIT] Evergreen ILS branch master updated. 2d5002c76cf31369d3c3745019ca5904831a07f7

Evergreen Git git at git.evergreen-ils.org
Fri Sep 1 13:26:13 EDT 2017


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  2d5002c76cf31369d3c3745019ca5904831a07f7 (commit)
       via  216bfe515ab8ecdce1aea21cd9da7458b8c59813 (commit)
       via  b460000b490b7b80bbef798c7405a2e5394a0360 (commit)
       via  a016beac0d5f5d4db3064dde674c0f38c08770d6 (commit)
       via  e4b050edc3a6d9ca8202f9747f673b00004483db (commit)
       via  7a8ee4c4bef0a3067eb08fcad2785fee21303595 (commit)
       via  1f9855413fbe4306ffdf3f64f079c35b6095371e (commit)
       via  4e49744a13272b949759b249dcf169c3b2e13db4 (commit)
       via  1fb2f3ff63434e43e40652a4b5734f92057755f5 (commit)
       via  74a7ff8148687fb942244d719139c98a689f9fbd (commit)
       via  20d7df2b54f3d7e9539da73a625b0dbaae705e0c (commit)
      from  cd22fa01980e6c9343012353bd9c43cf61f9838d (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 2d5002c76cf31369d3c3745019ca5904831a07f7
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Sep 1 13:26:05 2017 -0400

    Stamp upgrade scripts for native EDI support
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 5962835..ad1dd7c 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -90,7 +90,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1061', :eg_version); -- berick/kmlussier/cesardv/phasefx
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1067', :eg_version); -- berick/gmcharlt/miker
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.edi_attr_set.sql b/Open-ILS/src/sql/Pg/upgrade/1062.schema.edi_attr_set.sql
similarity index 92%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema.edi_attr_set.sql
rename to Open-ILS/src/sql/Pg/upgrade/1062.schema.edi_attr_set.sql
index 970f542..bbf7e7a 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.edi_attr_set.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1062.schema.edi_attr_set.sql
@@ -1,6 +1,6 @@
 BEGIN;
 
--- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('1062', :eg_version);
 
 CREATE TABLE acq.edi_attr (
     key     TEXT PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.inheritance-constraint-trigger.sql b/Open-ILS/src/sql/Pg/upgrade/1063.schema.inheritance-constraint-trigger.sql
similarity index 95%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema.inheritance-constraint-trigger.sql
rename to Open-ILS/src/sql/Pg/upgrade/1063.schema.inheritance-constraint-trigger.sql
index 94f4000..06a8e80 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.inheritance-constraint-trigger.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1063.schema.inheritance-constraint-trigger.sql
@@ -1,5 +1,7 @@
 BEGIN;
 
+SELECT evergreen.upgrade_deps_block_check('1063', :eg_version);
+
 DO $temp$
 DECLARE
 	r RECORD;
diff --git a/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.issuance_scap_fkey.sql b/Open-ILS/src/sql/Pg/upgrade/1064.schema.issuance_scap_fkey.sql
similarity index 90%
rename from Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.issuance_scap_fkey.sql
rename to Open-ILS/src/sql/Pg/upgrade/1064.schema.issuance_scap_fkey.sql
index d27f8bc..f30f3b5 100644
--- a/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.issuance_scap_fkey.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1064.schema.issuance_scap_fkey.sql
@@ -1,5 +1,7 @@
 BEGIN;
 
+SELECT evergreen.upgrade_deps_block_check('1064', :eg_version);
+
 ALTER TABLE serial.issuance DROP CONSTRAINT IF EXISTS issuance_caption_and_pattern_fkey;
 
 -- Using NOT VALID and VALIDATE CONSTRAINT limits the impact to concurrent work.
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.serial_pattern_templates.sql b/Open-ILS/src/sql/Pg/upgrade/1065.schema.serial_pattern_templates.sql
similarity index 92%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema.serial_pattern_templates.sql
rename to Open-ILS/src/sql/Pg/upgrade/1065.schema.serial_pattern_templates.sql
index d396682..c9795e8 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.serial_pattern_templates.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1065.schema.serial_pattern_templates.sql
@@ -1,6 +1,6 @@
 BEGIN;
 
--- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('1065', :eg_version);
 
 CREATE TABLE serial.pattern_template (
     id            SERIAL PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.spt_perms.sql b/Open-ILS/src/sql/Pg/upgrade/1066.data.spt_perms.sql
similarity index 90%
rename from Open-ILS/src/sql/Pg/upgrade/YYYY.data.spt_perms.sql
rename to Open-ILS/src/sql/Pg/upgrade/1066.data.spt_perms.sql
index 2ceef91..6c076de 100644
--- a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.spt_perms.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1066.data.spt_perms.sql
@@ -1,6 +1,6 @@
 BEGIN;
 
--- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('1066', :eg_version);
 
 INSERT INTO permission.perm_list ( id, code, description ) VALUES
  ( 593, 'ADMIN_SERIAL_PATTERN_TEMPLATE', oils_i18n_gettext( 593,
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.edi_attr_set.sql b/Open-ILS/src/sql/Pg/upgrade/1067.data.edi_attr_set.sql
similarity index 98%
rename from Open-ILS/src/sql/Pg/upgrade/YYYY.data.edi_attr_set.sql
rename to Open-ILS/src/sql/Pg/upgrade/1067.data.edi_attr_set.sql
index 8ca7643..aaaa6b3 100644
--- a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.edi_attr_set.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1067.data.edi_attr_set.sql
@@ -1,6 +1,6 @@
 BEGIN;
 
--- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('1067', :eg_version);
 
 INSERT INTO acq.edi_attr (key, label) VALUES
     ('INCLUDE_PO_NAME', 

commit 216bfe515ab8ecdce1aea21cd9da7458b8c59813
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Aug 23 13:07:33 2017 -0400

    LP#1373690 EDI attribute set clone operation
    
    Support cloning existing attribute sets via a new 'Clone "<existing set
    name>"' action in the EDI attr set editor.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/templates/staff/admin/acq/t_edi_attr_set.tt2 b/Open-ILS/src/templates/staff/admin/acq/t_edi_attr_set.tt2
index c70baef..79b2d31 100644
--- a/Open-ILS/src/templates/staff/admin/acq/t_edi_attr_set.tt2
+++ b/Open-ILS/src/templates/staff/admin/acq/t_edi_attr_set.tt2
@@ -17,6 +17,10 @@
             <a href='' ng-click="new_set()">
               [% l('New Attribute Set...') %]</a>
           </li>
+          <li ng-if="!cur_attr_set.isnew()">
+            <a href='' ng-click="clone_set(cur_attr_set)">
+              [% l('Clone "[_1]"', '{{cur_attr_set.label()}}') %]</a>
+          </li>
           <li class="divider"></li>
           <li ng-repeat="set in attr_sets">
             <a href='' ng-click="select_set(set)">{{set.label()}}</a>
diff --git a/Open-ILS/web/js/ui/default/staff/admin/acq/app.js b/Open-ILS/web/js/ui/default/staff/admin/acq/app.js
index de8dddc..1ff496a 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/acq/app.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/acq/app.js
@@ -144,6 +144,8 @@ function($scope , $q , egCore , ngToast , egConfirmDialog) {
         angular.forEach($scope.attr_sets, function(set) {
             console.debug('inspecting attr set ' + set.label());
 
+            if (!set.label()) return; // skip (new) unnamed sets
+
             // find maps that need deleting
             angular.forEach(set.attr_maps(), function(oldmap) {
                 if (!set._local_map[oldmap.attr()]) {
@@ -248,6 +250,23 @@ function($scope , $q , egCore , ngToast , egConfirmDialog) {
         });
     }
 
+    $scope.clone_set = function(source_set) {
+        var set = new egCore.idl.aeas();
+        set.isnew(true);
+        set.attr_maps([]);
+        set._local_map = {};
+
+        // Copy attr info from cloned attr set. No need to create the
+        // maps now, just indicate in the local mapping that attr maps
+        // are pending.
+        angular.forEach(source_set.attr_maps(), function(map) {
+            set._local_map[map.attr()] = true;
+        });
+
+        $scope.select_set(set);
+        $scope.attr_sets.push(set);
+    }
+
     load_data();
 }])
 

commit b460000b490b7b80bbef798c7405a2e5394a0360
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Tue Aug 22 16:13:54 2017 -0400

    LP#1373690: add USE_ID_FOR_OWNING_LIB EDI attribute
    
    This attribute specifies that the copy owning library
    should have its ID emitted rather than its shortname, which can
    be used for Baker & Taylor, which imposes a five-character limit
    on certain fields. This takes effect only when the INCLUDE_OWNING_LIB
    EDI attribute is also in effect.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Utils/EDIWriter.pm b/Open-ILS/src/perlmods/lib/OpenILS/Utils/EDIWriter.pm
index 3b4cdfd..713d7c0 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Utils/EDIWriter.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Utils/EDIWriter.pm
@@ -324,7 +324,11 @@ sub compile_copy {
     my $fund = $copy->fund ? $copy->fund->code : '';
     my $item_type = $copy->circ_modifier || '';
     my $call_number = $copy->cn_label || '';
-    my $owning_lib = $copy->owning_lib ? $copy->owning_lib->shortname : '';
+    my $owning_lib = $copy->owning_lib ?
+                        $self->{compiled}->{edi_attrs}->{USE_ID_FOR_OWNING_LIB} ?
+                        $copy->owning_lib->id :
+                        $copy->owning_lib->shortname :
+                     '';
     my $location = $copy->location ? $copy->location->name : '';
     my $collection_code = $copy->collection_code || '';
     my $barcode = $copy->barcode || '';
diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index d8dbd03..cad9960 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -17215,6 +17215,9 @@ INSERT INTO acq.edi_attr (key, label) VALUES
     ('INCLUDE_OWNING_LIB', 
         oils_i18n_gettext('INCLUDE_OWNING_LIB', 
         'Orders Include Copy Owning Library', 'aea', 'label')),
+    ('USE_ID_FOR_OWNING_LIB',
+        oils_i18n_gettext('USE_ID_FOR_OWNING_LIB',
+        'Emit Owning Library ID Rather Than Short Name. Takes effect only if INCLUDE_OWNING_LIB is in use', 'aea', 'label')),
     ('INCLUDE_QUANTITY', 
         oils_i18n_gettext('INCLUDE_QUANTITY', 
         'Orders Include Copy Quantities', 'aea', 'label')),
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.edi_attr_set.sql b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.edi_attr_set.sql
index 8e5c4b6..8ca7643 100644
--- a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.edi_attr_set.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.edi_attr_set.sql
@@ -30,6 +30,9 @@ INSERT INTO acq.edi_attr (key, label) VALUES
     ('INCLUDE_OWNING_LIB', 
         oils_i18n_gettext('INCLUDE_OWNING_LIB', 
         'Orders Include Copy Owning Library', 'aea', 'label')),
+    ('USE_ID_FOR_OWNING_LIB',
+        oils_i18n_gettext('USE_ID_FOR_OWNING_LIB',
+        'Emit Owning Library ID Rather Than Short Name. Takes effect only if INCLUDE_OWNING_LIB is in use', 'aea', 'label')),
     ('INCLUDE_QUANTITY', 
         oils_i18n_gettext('INCLUDE_QUANTITY', 
         'Orders Include Copy Quantities', 'aea', 'label')),

commit a016beac0d5f5d4db3064dde674c0f38c08770d6
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Tue Aug 22 16:03:24 2017 -0400

    LP#1373690: fix typos
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index b42d580..d8dbd03 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -17190,7 +17190,7 @@ VALUES (
 INSERT INTO acq.edi_attr (key, label) VALUES
     ('INCLUDE_PO_NAME', 
         oils_i18n_gettext('INCLUDE_PO_NAME', 
-        'Oders Include PO Name', 'aea', 'label')),
+        'Orders Include PO Name', 'aea', 'label')),
     ('INCLUDE_COPIES', 
         oils_i18n_gettext('INCLUDE_COPIES', 
         'Orders Include Copy Data', 'aea', 'label')),
@@ -17262,7 +17262,7 @@ INSERT INTO acq.edi_attr (key, label) VALUES
         'Lineitem Identifier Fields (LIN/PIA) Use Vendor-Encoded ID Value When Available', 'aea', 'label')),
     ('LINEITEM_REF_ID_ONLY',
         oils_i18n_gettext('LINEITEM_REF_ID_ONLY',
-        'Lineitem Reference Feld (RFF) Uses Lineitem ID Only', 'aea', 'label'))
+        'Lineitem Reference Field (RFF) Uses Lineitem ID Only', 'aea', 'label'))
 
 ;
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.edi_attr_set.sql b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.edi_attr_set.sql
index a38a129..8e5c4b6 100644
--- a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.edi_attr_set.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.edi_attr_set.sql
@@ -5,7 +5,7 @@ BEGIN;
 INSERT INTO acq.edi_attr (key, label) VALUES
     ('INCLUDE_PO_NAME', 
         oils_i18n_gettext('INCLUDE_PO_NAME', 
-        'Oders Include PO Name', 'aea', 'label')),
+        'Orders Include PO Name', 'aea', 'label')),
     ('INCLUDE_COPIES', 
         oils_i18n_gettext('INCLUDE_COPIES', 
         'Orders Include Copy Data', 'aea', 'label')),
@@ -77,7 +77,7 @@ INSERT INTO acq.edi_attr (key, label) VALUES
         'Lineitem Identifier Fields (LIN/PIA) Use Vendor-Encoded ID Value When Available', 'aea', 'label')),
     ('LINEITEM_REF_ID_ONLY',
         oils_i18n_gettext('LINEITEM_REF_ID_ONLY',
-        'Lineitem Reference Feld (RFF) Uses Lineitem ID Only', 'aea', 'label'))
+        'Lineitem Reference Field (RFF) Uses Lineitem ID Only', 'aea', 'label'))
 
 ;
 

commit e4b050edc3a6d9ca8202f9747f673b00004483db
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Tue Aug 22 16:00:47 2017 -0400

    LP#1373690: normalize IMD values better
    
    The EDItEUR book ORDERS message allows two 35-character
    item description data elements in IMD segments; this patch
    accounts for that. It also, for IMD fields, uses the EDIFACT
    release character to escape certain characters, better matching
    how the Ruby EDI translator did it.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Utils/EDIWriter.pm b/Open-ILS/src/perlmods/lib/OpenILS/Utils/EDIWriter.pm
index 2b94467..3b4cdfd 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Utils/EDIWriter.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Utils/EDIWriter.pm
@@ -65,7 +65,16 @@ sub get_po {
     ]);
 }
 
-sub escape_edi {
+sub add_release_characters {
+    my ($self, $value) = @_;
+    return '' if (not defined $value || ref($value));
+
+    # escape ? ' + : with the release character ?
+    $value =~ s/([\?'\+:])/?$1/g;
+
+    return $value;
+}
+sub escape_edi_imd {
     my ($self, $value) = @_;
     return '' if (not defined $value || ref($value));
 
@@ -79,16 +88,28 @@ sub escape_edi {
     # LP #812593).
     $value =~ s/[\[\]]//g;
 
-    # Characters [? + ' \ : <newline>] are all potentially problematic for 
+    # Characters [\ <newline>] are all potentially problematic for 
     # EDI messages, regardless of their position in the string.
-    # Safest to simply remove them.
-    $value =~ s/[\\\?\+':]//g;
+    # Safest to simply remove them. Note that unlike escape_edi(),
+    # we're not stripping out +, ', :, and + because we'll escape
+    # them when buidling IMD segments
+    $value =~ s/[\\]//g;
 
     # Replace newlines with spaces.
     $value =~ s/\n/ /g;
 
     return $value;
 }
+sub escape_edi {
+    my ($self, $value) = @_;
+
+    my $str = $self->escape_edi_imd($value);
+
+    # further strip + ' : +
+    $str =~ s/[\?\+':]//g;
+
+    return $str;
+}
 
 # Returns an EDI-escaped version of the requested lineitem attribute
 # value.  If $attr_type is not set, the first attribute found matching 
@@ -105,6 +126,20 @@ sub get_li_attr {
     return '';
 }
 
+# Like get_li_attr, but don't strip out ? + : ' as we'll
+# escape them later
+sub get_li_attr_imd {
+    my ($self, $li, $attr_name, $attr_type) = @_;
+
+    for my $attr (@{$li->attributes}) {
+        next unless $attr->attr_name eq $attr_name;
+        next if $attr_type && $attr->attr_type ne $attr_type;
+        return $self->escape_edi_imd($attr->attr_value);
+    }
+
+    return '';
+}
+
 # Generates a HASH version of the PO with all of the data necessary
 # to generate an EDI message from the PO.
 sub compile_po {
@@ -264,7 +299,7 @@ sub compile_li {
     $self->set_li_order_ident($li, $li_hash);
 
     for my $name (qw/title author edition pubdate publisher pagination/) {
-        $li_hash->{$name} = $self->get_li_attr($li, $name);
+        $li_hash->{$name} = $self->get_li_attr_imd($li, $name);
     }
 
     $self->compile_copies($li, $li_hash);
@@ -345,8 +380,8 @@ sub compile_copy {
     });
 }
 
-# IMD fields are limited to 70 chars per value.  Any values longer
-# should be carried via repeating IMD fields.
+# IMD fields are limited to 70 chars per value over two DEs.
+# Any values longer # should be carried via repeating IMD fields.
 # IMD fields should only display the +::: when a value is present
 sub IMD {
     my ($self, $code, $value) = @_;
@@ -359,7 +394,16 @@ sub IMD {
     if ($value) {
         my $s = '';
         for my $part ($value =~ m/.{1,70}/g) {
-            $s .= "IMD+F+$code+:::$part'\n"; }
+            my $de;
+            if (length($part) > 35) {
+                $de = $self->add_release_characters(substr($part, 0, 35)) .
+                      ':' .
+                      $self->add_release_characters(substr($part, 35));
+            } else {
+                $de = $self->add_release_characters($part);
+            }
+            $s .= "IMD+F+$code+:::$de'\n";
+        }
         return $s;
 
     } else {

commit 7a8ee4c4bef0a3067eb08fcad2785fee21303595
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Aug 18 11:51:39 2017 -0400

    LP#1373690 EDI 'use_attrs' configuration option
    
    Adds a new configuration option for EDI accounts specifying wether
    purchase orders delivered via a given account should be constructed
    using the new EDI Attributes in lieu of the traditional JEDI A/T
    template.
    
    The PurchaseOrderEDIRequired A/T validator now bypasses orders with
    use_attrs=true accounts.  The new edi_order_pusher.pl script only
    process providers whose default EDI accounts have use_attrs=true (except
    when a specific PO ID is provided).
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 7f6e192..77871ff 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -9214,6 +9214,7 @@ SELECT  usr,
 			<field name="vendacct"      reporter:datatype="text"      reporter:label="Vendor Account Number"/>
 			<field name="vendcode"      reporter:datatype="text"      reporter:label="Vendor Assigned Code"/>
 			<field name="attr_set"      reporter:datatype="link"      reporter:label="EDI Attribute Set"/>
+			<field name="use_attrs"     reporter:datatype="bool"      reporter:label="Use EDI Attributes"/>
 		</fields>
 		<links>
 			<link field="provider" reltype="has_a" key="id" map="" class="acqpro"/>
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Validator/Acq/PurchaseOrderEDIRequired.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Validator/Acq/PurchaseOrderEDIRequired.pm
index 211d75f..a0ae31f 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Validator/Acq/PurchaseOrderEDIRequired.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Validator/Acq/PurchaseOrderEDIRequired.pm
@@ -16,11 +16,11 @@ sub handler {
             new_editor->retrieve_acq_provider($po->provider);
 
     return 1 if 
-        ($po->state eq 'on-order' or
-         $po->state eq 'retry'      ) and 
-        $provider                     and 
-        $provider->edi_default        and 
-        $U->is_true($provider->active);
+        ($po->state eq 'on-order' || $po->state eq 'retry')
+        and $provider
+        and $provider->edi_default
+        and $U->is_true($provider->active)
+        and !$U->is_true($provider->edi_default->use_attrs);
 
     return 0;
 }
diff --git a/Open-ILS/src/sql/Pg/200.schema.acq.sql b/Open-ILS/src/sql/Pg/200.schema.acq.sql
index 7fa60e3..c0670bb 100644
--- a/Open-ILS/src/sql/Pg/200.schema.acq.sql
+++ b/Open-ILS/src/sql/Pg/200.schema.acq.sql
@@ -763,7 +763,8 @@ CREATE TABLE acq.edi_account (      -- similar tables can extend remote_account
     in_dir      TEXT,   -- incoming messages dir (probably different than config.remote_account.path, the outgoing dir)
     vendcode    TEXT,
     vendacct    TEXT,
-    attr_set    INTEGER REFERENCES acq.edi_attr_set(id) -- NULL OK
+    attr_set    INTEGER REFERENCES acq.edi_attr_set(id), -- NULL OK
+    use_attrs   BOOLEAN NOT NULL DEFAULT FALSE
 ) INHERITS (config.remote_account);
 
 -- We need a UNIQUE constraint here also, to support the FK from acq.provider.edi_default
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.edi_attr_set.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.edi_attr_set.sql
index be1aa23..970f542 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.edi_attr_set.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.edi_attr_set.sql
@@ -23,8 +23,9 @@ CREATE TABLE acq.edi_attr_set_map (
 
 -- An attr_set is not strictly required, since some edi_accounts/vendors 
 -- may not need to apply any attributes.
-ALTER TABLE acq.edi_account ADD COLUMN attr_set 
-    INTEGER REFERENCES acq.edi_attr_set(id);
+ALTER TABLE acq.edi_account 
+    ADD COLUMN attr_set INTEGER REFERENCES acq.edi_attr_set(id),
+    ADD COLUMN use_attrs BOOLEAN NOT NULL DEFAULT FALSE;
 
 COMMIT;
 
diff --git a/Open-ILS/src/support-scripts/edi_order_pusher.pl b/Open-ILS/src/support-scripts/edi_order_pusher.pl
index 380ed49..3d383f2 100755
--- a/Open-ILS/src/support-scripts/edi_order_pusher.pl
+++ b/Open-ILS/src/support-scripts/edi_order_pusher.pl
@@ -49,7 +49,8 @@ sub help {
         1. PO must be activated.
         2. PO provider must be active.
         3. PO must use a provider that supports EDI delivery (via edi_default)
-        4. PO must have no EDI ORDERS messages attached or, if it does, 
+        4. EDI account linked to provider must have 'use_attrs' set to true.
+        5. PO must have no EDI ORDERS messages attached or, if it does, 
            the message has a status of "retry".
 
     Usage:
@@ -118,7 +119,12 @@ if ($po_id) {
                     type => 'left',
                     filter => {message_type => 'ORDERS'}
                 },
-                acqpro => {}
+                acqpro => {
+                    join => {
+                        acqedi => {
+                        }
+                    }
+                }
             }
         },
         where => {
@@ -129,6 +135,9 @@ if ($po_id) {
                 active => 't', 
                 edi_default => {'!=' => undef}
             },
+            '+acqedi' => {
+                use_attrs => 't'
+            },
             '+acqedim' => {
                 '-or' => [
                     {id => undef}, # no ORDERS message exists
diff --git a/docs/RELEASE_NOTES_NEXT/Administration/acq-edi-attrs.adoc b/docs/RELEASE_NOTES_NEXT/Administration/acq-edi-attrs.adoc
index db69841..6100824 100644
--- a/docs/RELEASE_NOTES_NEXT/Administration/acq-edi-attrs.adoc
+++ b/docs/RELEASE_NOTES_NEXT/Administration/acq-edi-attrs.adoc
@@ -36,3 +36,15 @@ attributes.
 . After moving to edi_order_pusher.pl, the JEDI Action/Trigger event
   definition is no longer required.  It can be disabled.
 
+Migration
++++++++++
+
+EDI accounts have a new boolean field "Use EDI Attributes" (use_attrs) that 
+specifies whether PO's generated via the account should be built using 
+EDI attributes or fall back to traditional JEDI A/T template generation.
+
+This allows sites to activate EDI attributes on a per-account basis, making 
+it possible to migrate piecemeal to EDI attributes.  For the initial roll
+out of this new features, no accounts will be configured to use EDI 
+attributes by default.  
+

commit 1f9855413fbe4306ffdf3f64f079c35b6095371e
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Jun 22 14:49:14 2017 -0400

    LP#1373690 Midwest Library Service EDI attrs
    
    Adds a new default collection of EDI attributes for Midwest Library
    Service.  This includes 2 new attributes to support their EDI format:
    
    1. BUYER_ID_ONLY_VENDCODE -- similar to BUYER_ID_INCLUDE_VENDCODE,
       except the buyer SAN is excluded from the NAD+BY segement.
    
    2. INCLUDE_EMPTY_IMD_VALUES -- Render IMD+ fields (title, author,
       edition, etc.) as if they contain values even when they don't.  Empty
       values are included as single-space strings.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Utils/EDIWriter.pm b/Open-ILS/src/perlmods/lib/OpenILS/Utils/EDIWriter.pm
index 6d825f1..2b94467 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Utils/EDIWriter.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Utils/EDIWriter.pm
@@ -131,10 +131,14 @@ sub compile_po {
             for @{$po->provider->edi_default->attr_set->attr_maps}
     }
 
-    $compiled{buyer_code} = 
-        $compiled{edi_attrs}->{BUYER_ID_INCLUDE_VENDCODE} ? # B&T
-        $compiled{vendor_san}.' '.$po->provider->edi_default->vendcode :
-        $po->provider->edi_default->vendacct;
+    $compiled{buyer_code} = $po->provider->edi_default->vendacct;
+
+    $compiled{buyer_code} = # B&T
+        $compiled{vendor_san}.' '.$po->provider->edi_default->vendcode
+        if $compiled{edi_attrs}->{BUYER_ID_INCLUDE_VENDCODE};
+
+    $compiled{buyer_code} = $po->provider->edi_default->vendcode
+        if $compiled{edi_attrs}->{BUYER_ID_ONLY_VENDCODE}; # MLS
 
     push(@{$compiled{lineitems}}, 
         $self->compile_li($_)) for @{$po->lineitems};
@@ -346,6 +350,12 @@ sub compile_copy {
 # IMD fields should only display the +::: when a value is present
 sub IMD {
     my ($self, $code, $value) = @_;
+
+    $value = ' ' if (
+        $value eq '' &&
+        $self->{compiled}->{edi_attrs}->{INCLUDE_EMPTY_IMD_VALUES}
+    );
+
     if ($value) {
         my $s = '';
         for my $part ($value =~ m/.{1,70}/g) {
@@ -388,8 +398,10 @@ BGM+220+$c{po_id}+9'
 DTM+137:$date:102'
 EDI
 
-    $edi .= "NAD+BY+$c{org_unit_san}::31B'\n"
-        unless $self->{compiled}->{edi_attrs}->{BUYER_ID_INCLUDE_VENDCODE};
+    $edi .= "NAD+BY+$c{org_unit_san}::31B'\n" unless (
+        $self->{compiled}->{edi_attrs}->{BUYER_ID_ONLY_VENDCODE} ||
+        $self->{compiled}->{edi_attrs}->{BUYER_ID_INCLUDE_VENDCODE}
+    );
 
     $edi .= <<EDI;
 NAD+BY+$c{buyer_code}::91'
diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index 8523778..b42d580 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -17224,6 +17224,9 @@ INSERT INTO acq.edi_attr (key, label) VALUES
     ('BUYER_ID_INCLUDE_VENDCODE', 
         oils_i18n_gettext('BUYER_ID_INCLUDE_VENDCODE', 
         'Buyer ID Qualifier Includes Vendcode', 'aea', 'label')),
+    ('BUYER_ID_ONLY_VENDCODE', 
+        oils_i18n_gettext('BUYER_ID_ONLY_VENDCODE', 
+        'Buyer ID Qualifier Only Contains Vendcode', 'aea', 'label')),
     ('INCLUDE_BIB_EDITION', 
         oils_i18n_gettext('INCLUDE_BIB_EDITION', 
         'Order Lineitems Include Edition Info', 'aea', 'label')),
@@ -17236,6 +17239,9 @@ INSERT INTO acq.edi_attr (key, label) VALUES
     ('COPY_SPEC_CODES', 
         oils_i18n_gettext('COPY_SPEC_CODES', 
         'Order Lineitem Notes Include Copy Spec Codes', 'aea', 'label')),
+    ('INCLUDE_EMPTY_IMD_VALUES', 
+        oils_i18n_gettext('INCLUDE_EMPTY_IMD_VALUES',
+        'Lineitem Title, Author, etc. Fields Are Present Even if Empty', 'aea', 'label')),
     ('INCLUDE_EMPTY_LI_NOTE', 
         oils_i18n_gettext('INCLUDE_EMPTY_LI_NOTE', 
         'Order Lineitem Notes Always Present (Even if Empty)', 'aea', 'label')),
@@ -17266,6 +17272,7 @@ INSERT INTO acq.edi_attr_set (id, label) VALUES (3, 'Brodart Default');
 INSERT INTO acq.edi_attr_set (id, label) VALUES (4, 'Midwest Tape Default');
 INSERT INTO acq.edi_attr_set (id, label) VALUES (5, 'ULS Default');
 INSERT INTO acq.edi_attr_set (id, label) VALUES (6, 'Recorded Books Default');
+INSERT INTO acq.edi_attr_set (id, label) VALUES (7, 'Midwest Library Service');
 
 -- carve out space for mucho defaults
 SELECT SETVAL('acq.edi_attr_set_id_seq'::TEXT, 1000);
@@ -17331,6 +17338,13 @@ INSERT INTO acq.edi_attr_set_map (attr_set, attr) VALUES
     (6, 'INCLUDE_COLLECTION_CODE'),
     (6, 'INCLUDE_OWNING_LIB'),
     (6, 'INCLUDE_QUANTITY'),
-    (6, 'INCLUDE_BIB_PAGINATION')
+    (6, 'INCLUDE_BIB_PAGINATION'),
+
+    -- Midwest Library Service
+    (7, 'INCLUDE_BIB_AUTHOR'),
+    (7, 'INCLUDE_BIB_EDITION'),
+    (7, 'BUYER_ID_ONLY_VENDCODE'),
+    (7, 'INCLUDE_EMPTY_IMD_VALUES')
+
 ;
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.edi_attr_set.sql b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.edi_attr_set.sql
index 7bb42f6..a38a129 100644
--- a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.edi_attr_set.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.edi_attr_set.sql
@@ -39,6 +39,9 @@ INSERT INTO acq.edi_attr (key, label) VALUES
     ('BUYER_ID_INCLUDE_VENDCODE', 
         oils_i18n_gettext('BUYER_ID_INCLUDE_VENDCODE', 
         'Buyer ID Qualifier Includes Vendcode', 'aea', 'label')),
+    ('BUYER_ID_ONLY_VENDCODE', 
+        oils_i18n_gettext('BUYER_ID_ONLY_VENDCODE', 
+        'Buyer ID Qualifier Only Contains Vendcode', 'aea', 'label')),
     ('INCLUDE_BIB_EDITION', 
         oils_i18n_gettext('INCLUDE_BIB_EDITION', 
         'Order Lineitems Include Edition Info', 'aea', 'label')),
@@ -51,6 +54,9 @@ INSERT INTO acq.edi_attr (key, label) VALUES
     ('COPY_SPEC_CODES', 
         oils_i18n_gettext('COPY_SPEC_CODES', 
         'Order Lineitem Notes Include Copy Spec Codes', 'aea', 'label')),
+    ('INCLUDE_EMPTY_IMD_VALUES', 
+        oils_i18n_gettext('INCLUDE_EMPTY_IMD_VALUES',
+        'Lineitem Title, Author, etc. Fields Are Present Even if Empty', 'aea', 'label')),
     ('INCLUDE_EMPTY_LI_NOTE', 
         oils_i18n_gettext('INCLUDE_EMPTY_LI_NOTE', 
         'Order Lineitem Notes Always Present (Even if Empty)', 'aea', 'label')),
@@ -81,6 +87,7 @@ INSERT INTO acq.edi_attr_set (id, label) VALUES (3, 'Brodart Default');
 INSERT INTO acq.edi_attr_set (id, label) VALUES (4, 'Midwest Tape Default');
 INSERT INTO acq.edi_attr_set (id, label) VALUES (5, 'ULS Default');
 INSERT INTO acq.edi_attr_set (id, label) VALUES (6, 'Recorded Books Default');
+INSERT INTO acq.edi_attr_set (id, label) VALUES (7, 'Midwest Library Service');
 
 -- carve out space for mucho defaults
 SELECT SETVAL('acq.edi_attr_set_id_seq'::TEXT, 1000);
@@ -146,7 +153,13 @@ INSERT INTO acq.edi_attr_set_map (attr_set, attr) VALUES
     (6, 'INCLUDE_COLLECTION_CODE'),
     (6, 'INCLUDE_OWNING_LIB'),
     (6, 'INCLUDE_QUANTITY'),
-    (6, 'INCLUDE_BIB_PAGINATION')
+    (6, 'INCLUDE_BIB_PAGINATION'),
+
+    -- Midwest Library Service
+    (7, 'INCLUDE_BIB_AUTHOR'),
+    (7, 'INCLUDE_BIB_EDITION'),
+    (7, 'BUYER_ID_ONLY_VENDCODE'),
+    (7, 'INCLUDE_EMPTY_IMD_VALUES')
 ;
 
 

commit 4e49744a13272b949759b249dcf169c3b2e13db4
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Nov 3 11:19:36 2016 -0400

    LP#1373690 EDI attributes release notes
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/docs/RELEASE_NOTES_NEXT/Administration/acq-edi-attrs.adoc b/docs/RELEASE_NOTES_NEXT/Administration/acq-edi-attrs.adoc
new file mode 100644
index 0000000..db69841
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Administration/acq-edi-attrs.adoc
@@ -0,0 +1,38 @@
+New EDI Order Generator 
+^^^^^^^^^^^^^^^^^^^^^^^
+
+Configuration
++++++++++++++
+
+. New database tables exist for configuring vendor-specific EDI order 
+attributes.
+
+ * acq.edi_attr 
+  ** List of EDI order generation toggles, e.g. "INCLUDE_COPIES" to add 
+     GIR segments
+ * acq.edi_attr_set
+  ** Collection of edi_attr's.  Each edi_account may be linked to one
+     edi_attr_set.
+  ** One edi_attr_set per known vendor is added to the stock data, matching
+     the stock configuration found in the JEDI template.
+ * acq.edi_attr_set_map
+  ** Link between edi_attr's and edi_attr_set's.
+
+. EDI Attribute Sets are manged via a new (browser client only) configuration
+  interface at Administration -> Acquisistions Administration -> EDI
+  Attribute Sets.
+
+. Each acq.edi_account should be linked to an acq.edi_attr_set.  If a link 
+  is not set, default values will be used.  Links between an EDI account
+  and an attribute set are managed in the EDI Accounts configuration 
+  interface.
+
+. Local modifications to the stock EG JEDI template are managed by modifying
+  and/or adding additional edi_att_set's as needed.
+
+. A new edi_order_pusher.pl script is added which replaces the functionality
+  of edi_pusher.pl.  edi_pusher.pl is still avaialable.
+
+. After moving to edi_order_pusher.pl, the JEDI Action/Trigger event
+  definition is no longer required.  It can be disabled.
+

commit 1fb2f3ff63434e43e40652a4b5734f92057755f5
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Mar 8 12:47:55 2017 -0500

    LP#1373690 EDI attribute sets admin UI
    
    1. Create new attribute sets
    2. Rename attribute Sets.
    3. Enable / Disable attributes for each attributes set.
    
    Found under Admin -> Acquisitions -> EDI Attribute Sets.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/templates/staff/admin/acq/index.tt2 b/Open-ILS/src/templates/staff/admin/acq/index.tt2
index f5a8b7a..4378935 100644
--- a/Open-ILS/src/templates/staff/admin/acq/index.tt2
+++ b/Open-ILS/src/templates/staff/admin/acq/index.tt2
@@ -8,8 +8,16 @@
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/eframe.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/admin/acq/app.js"></script>
 <link rel="stylesheet" href="[% ctx.base_path %]/staff/css/admin.css" />
+<script>
+angular.module('egCoreMod').run(['egStrings', function(s) {
+  s.ATTR_SET_SUCCESS = "[% l('Attribute Set Update Succeeded') %]";
+  s.ATTR_SET_ERROR = "[% l('Attribute Set Update Failed') %]";
+  s.ATTR_SET_DELETE_CONFIRM = "[% l('Delete Attribute Set?') %]";
+}]);
+</script>
 [% END %]
 
+
 <div ng-view></div>
 
 [% END %]
diff --git a/Open-ILS/src/templates/staff/admin/acq/t_edi_attr_set.tt2 b/Open-ILS/src/templates/staff/admin/acq/t_edi_attr_set.tt2
new file mode 100644
index 0000000..c70baef
--- /dev/null
+++ b/Open-ILS/src/templates/staff/admin/acq/t_edi_attr_set.tt2
@@ -0,0 +1,67 @@
+<div class="container-fluid" style="text-align:center">
+  <div class="alert alert-info alert-less-pad strong-text-2">
+    <span>[% l('EDI Attribute Sets') %]</span>
+  </div>
+</div>
+
+<div class="row">
+  <div class="col-md-4">
+    <div class="input-group">
+      <div class="input-group-btn" uib-dropdown>
+        <button type="button" class="btn btn-default" uib-dropdown-toggle>
+          [% l('Attribute Sets') %]
+          <span class="caret"></span>
+        </button>
+        <ul uib-dropdown-menu>
+          <li>
+            <a href='' ng-click="new_set()">
+              [% l('New Attribute Set...') %]</a>
+          </li>
+          <li class="divider"></li>
+          <li ng-repeat="set in attr_sets">
+            <a href='' ng-click="select_set(set)">{{set.label()}}</a>
+          </li>
+        </ul>
+      </div><!-- /btn-group -->
+      <input type="text" ng-if="!cur_attr_set"
+        class="form-control" disabled="disabled"
+        value="[% l('No Attribute Set Selected') %]"/>
+      <input type="text" ng-if="cur_attr_set"
+        class="form-control"
+        placeholder="[% l('Attribute Set Name...') %]"
+        ng-model-options="{ getterSetter: true }"
+        ng-model="cur_attr_set.label"/>
+    </div>
+  </div>
+  <div class="col-md-4">
+    <span class="pad-right">
+      <button class="btn btn-success" 
+        ng-disabled="save_in_progress"
+        ng-click="apply()">[% l('Apply Changes') %]</button>
+    </span>
+    <span class="pad-right">
+      <button class="btn btn-warning" 
+        ng-disabled="cur_attr_set_uses || save_in_progress"
+        ng-click="remove()">[% l('Delete Attribute Set') %]</button>
+    </span>
+    <span class="pad-right" style="font-style:italic">
+      [% l('Currently used by [_1] EDI account(s).', '{{cur_attr_set_uses}}') %]
+    </span>
+  </div>
+</div>
+
+<div class="pad-vert">
+  <div class="row" ng-repeat="attr in attrs | orderBy:'key()'"
+    ng-class="cur_attr_set._local_map[attr.key()] ? 'selected-row' : ''">
+    <div class="col-md-3">
+      <span class="pad-right-min">
+        <input type="checkbox" 
+          ng-model="cur_attr_set._local_map[attr.key()]"/>
+      </span>
+      <span>{{attr.key()}}</span>
+    </div>
+    <div class="col-md-9">{{attr.label()}}</div>
+  </div>
+</div>
+
+
diff --git a/Open-ILS/src/templates/staff/admin/acq/t_splash.tt2 b/Open-ILS/src/templates/staff/admin/acq/t_splash.tt2
index a873724..e9d2894 100644
--- a/Open-ILS/src/templates/staff/admin/acq/t_splash.tt2
+++ b/Open-ILS/src/templates/staff/admin/acq/t_splash.tt2
@@ -19,6 +19,7 @@
     ,[ l('Distribution Formulas'), "./admin/acq/conify/distribution_formula" ]
     ,[ l('EDI Accounts'), "./admin/acq/conify/edi_account" ]
     ,[ l('EDI Messages'), "./admin/acq/po/edi_messages" ]
+    ,[ l('EDI Attribute Sets'), "./admin/acq/edi_attr_set" ]
     ,[ l('Exchange Rates'), "./admin/acq/conify/exchange_rate" ]
     ,[ l('Fund Tags'), "./admin/acq/conify/fund_tag" ]
     ,[ l('Funding Sources'), "./admin/acq/funding_source/list" ]
diff --git a/Open-ILS/src/templates/staff/css/style.css.tt2 b/Open-ILS/src/templates/staff/css/style.css.tt2
index 9aa3eda..2369cd4 100644
--- a/Open-ILS/src/templates/staff/css/style.css.tt2
+++ b/Open-ILS/src/templates/staff/css/style.css.tt2
@@ -199,6 +199,13 @@ table.list tr.selected td { /* deprecated? */
   padding: 0px;
 }
 
+/* Useful for grid-like things that aren't proper grids.
+ * Mimics the grids color scheme. */
+.selected-row {
+  background-color: rgb(248, 248, 248);
+}
+
+
 /* ----------------------------------------------------------------------
  * Grid
  * ---------------------------------------------------------------------- */
diff --git a/Open-ILS/web/js/ui/default/staff/admin/acq/app.js b/Open-ILS/web/js/ui/default/staff/admin/acq/app.js
index 14dbe84..de8dddc 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/acq/app.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/acq/app.js
@@ -8,6 +8,12 @@ angular.module('egAcqAdmin',
     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); 
     var resolver = {delay : function(egStartup) {return egStartup.go()}};
 
+    $routeProvider.when('/admin/acq/edi_attr_set', {
+        templateUrl: './admin/acq/t_edi_attr_set',
+        controller: 'EDIAttrSet',
+        resolve : resolver
+    });
+
     var eframe_template = 
         '<eg-embed-frame allow-escape="true" min-height="min_height" url="acq_admin_url" handlers="funcs"></eg-embed-frame>';
 
@@ -60,3 +66,189 @@ function($scope , $routeParams , $location , egCore) {
 
 }])
 
+.controller('EDIAttrSet',
+       ['$scope','$q','egCore','ngToast','egConfirmDialog',
+function($scope , $q , egCore , ngToast , egConfirmDialog) {
+    
+    $scope.cur_attr_set = null;
+
+    // fetch all the data needed to render the page.
+    function load_data() {
+
+        return egCore.pcrud.retrieveAll('aea', {}, 
+            {atomic : true, authoritative : true})
+        .then(
+            function(attrs) { 
+                $scope.attrs = attrs 
+                return egCore.pcrud.retrieveAll('aeas', 
+                    {flesh : 1, flesh_fields : {'aeas' : ['attr_maps']}}, 
+                    {atomic : true, authoritative : true}
+                )
+            }
+
+        ).then(function(sets) { 
+            $scope.attr_sets = sets.sort(function(a, b) {
+                return a.label() < b.label() ? -1 : 1;
+            });
+
+            // create a simple true/false attr_set => attr mapping
+            var select_me;
+            angular.forEach(sets, function(set) {
+                set._local_map = {};
+                angular.forEach(set.attr_maps(), function(map) {
+                    set._local_map[map.attr()] = true;
+                })
+
+                if ($scope.cur_attr_set && set.id() 
+                        == $scope.cur_attr_set.id()) {
+                    select_me = set;
+                }
+            });
+
+            $scope.select_set(select_me || $scope.attr_sets[0]);
+        });
+    }
+
+    function create_sets() {
+        var new_sets = $scope.attr_sets.filter(function(set) { 
+            if (set.isnew() && set.label()) {
+                console.debug('creating new set: ' + set.label());
+                return true;
+            } 
+            return false;
+        });
+
+        if (new_sets.length == 0) return $q.when();
+
+        // create the new attrs sets and collect the newly generated 
+        // ID in the local data store.
+        return egCore.pcrud.apply(new_sets).then(
+            null,
+            function() { 
+                $scope.attr_sets = $scope.attr_sets.filter(
+                    function(set) { return (set.label() && !set.isnew()) });
+                return $q.reject();
+            },
+            function(new_set) { 
+                var old_set = new_sets.filter(function(s) {
+                    return (s.isnew() && s.label() == new_set.label()) })[0];
+                old_set.id(new_set.id());
+                old_set.isnew(false);
+            }
+        );
+    }
+
+    function modify_maps() {
+        var update_maps = [];
+
+        angular.forEach($scope.attr_sets, function(set) {
+            console.debug('inspecting attr set ' + set.label());
+
+            // find maps that need deleting
+            angular.forEach(set.attr_maps(), function(oldmap) {
+                if (!set._local_map[oldmap.attr()]) {
+                    console.debug('\tdeleting map for ' + oldmap.attr());
+                    oldmap.isdeleted(true);
+                    update_maps.push(oldmap);
+                }
+            });
+
+            // find maps that need creating
+            angular.forEach(set._local_map, function(value, key) {
+                if (!value) return;
+
+                var existing = set.attr_maps().filter(
+                    function(emap) { return emap.attr() == key })[0];
+
+                if (existing) return;
+
+                console.debug('\tcreating map for ' + key);
+
+                var newmap = new egCore.idl.aeasm();
+                newmap.isnew(true);
+                newmap.attr(key);
+                newmap.attr_set(set.id());
+                update_maps.push(newmap);
+            });
+        });
+
+        return egCore.pcrud.apply(update_maps);
+    }
+
+    // mark the currently selected attr set as the main display set.
+    $scope.select_set = function(set) {
+        $scope.cur_attr_set_uses = 0; // how many edi accounts use this set
+        if (set.isnew()) {
+            $scope.cur_attr_set = set;
+        } else {
+            egCore.pcrud.search('acqedi', {attr_set : set.id()}, {}, 
+                {idlist : true, atomic : true}
+            ).then(function(accts) {
+                $scope.cur_attr_set_uses = accts.length;
+                $scope.cur_attr_set = set;
+            });
+        }
+    }
+
+    $scope.new_set = function() {
+        var set = new egCore.idl.aeas();
+        set.isnew(true);
+        set.attr_maps([]);
+        set._local_map = {};
+        $scope.select_set(set);
+        $scope.attr_sets.push(set);
+    }
+
+    $scope.apply = function() {
+        $scope.save_in_progress = true;
+        create_sets()
+            .then(modify_maps)
+            .then(
+                function() { 
+                    ngToast.create(egCore.strings.ATTR_SET_SUCCESS) 
+                },
+                function() { 
+                    ngToast.warning(egCore.strings.ATTR_SET_ERROR);
+                    return $q.reject();
+                })
+            .then(load_data)
+            .finally(
+                function() { $scope.save_in_progress = false; }
+            );
+    }
+
+    // Delete the currently selected attr set.
+    // Attr set maps will cascade delete.
+    $scope.remove = function() {
+        egConfirmDialog.open(
+            egCore.strings.ATTR_SET_DELETE_CONFIRM, 
+            $scope.cur_attr_set.label()
+        ).result.then(function() {
+            $scope.save_in_progress = true;
+            (   // remove from server if necessary
+                $scope.cur_attr_set.isnew() ?
+                $q.when() :
+                egCore.pcrud.remove($scope.cur_attr_set)
+            ).then(
+                // remove from the local att_sets list
+                function() {
+                    ngToast.create(egCore.strings.ATTR_SET_SUCCESS);
+                    $scope.attr_sets = $scope.attr_sets.filter(
+                        function(set) {
+                            return set.id() != $scope.cur_attr_set.id() 
+                        }
+                    );
+                    $scope.cur_attr_set = $scope.attr_sets[0];
+                },
+                function() { ngToast.warning(egCore.strings.ATTR_SET_ERROR) }
+
+            ).finally(
+                function() { $scope.save_in_progress = false; }
+            );
+        });
+    }
+
+    load_data();
+}])
+
+
diff --git a/Open-ILS/web/js/ui/default/staff/services/pcrud.js b/Open-ILS/web/js/ui/default/staff/services/pcrud.js
index 66fdba9..72cb94e 100644
--- a/Open-ILS/web/js/ui/default/staff/services/pcrud.js
+++ b/Open-ILS/web/js/ui/default/staff/services/pcrud.js
@@ -91,7 +91,9 @@ angular.module('egCoreMod')
             this.session.disconnect();
         };
 
-        this.retrieve = function(fm_class, pkey, pcrud_ops) {
+        this.retrieve = function(fm_class, pkey, pcrud_ops, req_ops) {
+            req_ops = req_ops || {};
+            this.authoritative = req_ops.authoritative;
             return this._dispatch(
                 'open-ils.pcrud.retrieve.' + fm_class,
                 [egAuth.token(), pkey, pcrud_ops]
@@ -106,6 +108,7 @@ angular.module('egCoreMod')
 
         this.search = function (fm_class, search, pcrud_ops, req_ops) {
             req_ops = req_ops || {};
+            this.authoritative = req_ops.authoritative;
 
             var return_type = req_ops.idlist ? 'id_list' : 'search';
             var method = 'open-ils.pcrud.' + return_type + '.' + fm_class;
@@ -179,7 +182,7 @@ angular.module('egCoreMod')
                 },
 
                 // main body error handler
-                function() {}, 
+                function() {deferred.reject()}, 
 
                 // main body notify() handler
                 function(data) {deferred.notify(data)}
@@ -287,7 +290,8 @@ angular.module('egCoreMod')
                     self.cud_last = data;
                     self.cud_deferred.notify(data);
                     self._CUD_next_request();
-                }
+                },
+                self.cud_deferred.reject
             );
            
         };
diff --git a/Open-ILS/web/js/ui/default/staff/services/ui.js b/Open-ILS/web/js/ui/default/staff/services/ui.js
index 51aac05..0c385fa 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -398,6 +398,7 @@ function($uibModal, $interpolate) {
     var service = {};
 
     service.open = function(title, message, msg_scope, ok_button_label, cancel_button_label) {
+        msg_scope = msg_scope || {};
         return $uibModal.open({
             templateUrl: './share/t_confirm_dialog',
             controller: ['$scope', '$uibModalInstance',

commit 74a7ff8148687fb942244d719139c98a689f9fbd
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri May 27 15:46:27 2016 -0400

    LP#1373690 EDI ORDERS generator script
    
    Adds a new edi_order_pusher.pl script for generating EDI ORDERS from
    purchase orders via EDIWriter.pm.  This is a replacement for
    edi_pusher.pl, but edi_pusher.pl remains for backwards compat.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/support-scripts/edi_order_pusher.pl b/Open-ILS/src/support-scripts/edi_order_pusher.pl
new file mode 100755
index 0000000..380ed49
--- /dev/null
+++ b/Open-ILS/src/support-scripts/edi_order_pusher.pl
@@ -0,0 +1,225 @@
+#!/usr/bin/perl
+# ---------------------------------------------------------------
+# Copyright (C) 2016 King County Library System
+# Author: Bill Erickson <berickxx at gmail.com>
+#
+# Copied heavily from edi_pusher.pl
+#
+# 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 Getopt::Long;
+use OpenSRF::Utils::Logger q/$logger/;
+use OpenILS::Utils::Fieldmapper;
+use OpenILS::Application::Acq::EDI;
+use OpenILS::Utils::EDIWriter;
+
+my $osrf_config = '/openils/conf/opensrf_core.xml';
+my $po_id;
+my $test_mode;
+my $verbose;
+my $help;
+
+my $ops = GetOptions(
+    'osrf-config=s' => \$osrf_config,
+    'test-mode'     => \$test_mode,
+    'po-id=i'       => \$po_id,
+    'verbose'       => \$verbose,
+    'help'          => \$help
+);
+
+sub help {
+    print <<HELP;
+
+    Synopsis:
+
+        Generate and deliver 'ORDERS' EDI for purchase orders.  Unless a
+        specific PO is provided (via --po-id), EDI messages will be 
+        generated for all PO's that meet the following conditions:
+        
+        1. PO must be activated.
+        2. PO provider must be active.
+        3. PO must use a provider that supports EDI delivery (via edi_default)
+        4. PO must have no EDI ORDERS messages attached or, if it does, 
+           the message has a status of "retry".
+
+    Usage:
+
+        $0
+
+        --osrf-config [/openils/conf/opensrf_core.xml]
+
+        --test-mode
+            Prints EDI that would be sent to STDOUT.  No files are sent
+            and no edi_message's are created.
+
+        --po-id <po-id-value>
+            Process a specific PO instead of processing all available PO's
+
+        --verbose
+            Log debug info to STDOUT.  This script logs various information
+            via \$logger regardless of this option.
+
+        --help
+            Show this message.
+HELP
+    exit 0;
+}
+
+help() if $help or !$ops;
+
+# $lvl should match a $logger logging function.  E.g. 'info', 'error', etc.
+sub announce {
+    my $lvl = shift;
+	my $msg = shift;
+    $logger->$lvl($msg);
+
+    # always announce errors and warnings
+    return unless $verbose || $lvl =~ /error|warn/;
+
+    my $date_str = DateTime->now(time_zone => 'local')->strftime('%F %T');
+    print "$date_str $msg\n";
+}
+
+# connect to osrf...
+OpenSRF::System->bootstrap_client(config_file => $osrf_config);
+Fieldmapper->import(IDL => 
+    OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
+OpenILS::Utils::CStoreEditor::init();
+my $e = OpenILS::Utils::CStoreEditor->new;
+
+announce('debug', "FTP_PASSIVE is ".($ENV{FTP_PASSIVE} ? "ON" : "OFF"));
+
+my $po_ids;
+
+if ($po_id) {
+    # Caller provided a specific PO to process.
+    $po_ids = [$po_id];
+
+} else {
+    # Find PO's that have an order date set (i.e. are activated) and are 
+    # linked to active providers that support EDI orders, but has no 
+    # successful "ORDERS" edi_message attached.
+    
+    my $ids = $e->json_query({
+        select => {acqpo => ['id']},
+        from => {
+            acqpo => {
+                acqedim => {
+                    type => 'left',
+                    filter => {message_type => 'ORDERS'}
+                },
+                acqpro => {}
+            }
+        },
+        where => {
+            '+acqpo' => {
+                order_date => {'!=' => undef} # activated
+            },
+            '+acqpro' => {
+                active => 't', 
+                edi_default => {'!=' => undef}
+            },
+            '+acqedim' => {
+                '-or' => [
+                    {id => undef}, # no ORDERS message exists
+                    {status => 'retry'} # ORDERS needs re-sending
+                ]
+            }
+        }
+    });
+
+    $po_ids = [map {$_->{id}} @$ids];
+}
+
+if (!@$po_ids) {
+    announce('info', "No purchase orders to process");
+    exit 0;
+}
+
+for $po_id (@$po_ids) {
+
+    my $edi = OpenILS::Utils::EDIWriter->new->write($po_id);
+
+    if (!$edi) {
+        announce('error', "Unable to generate EDI for PO $po_id");
+        next;
+    }
+
+    if ($test_mode) {
+        # Caller just wants the EDI printed to STDOUT
+        print "EDI for PO $po_id:\n$edi\n";
+        next;
+    }
+
+    # this may be a retry attempt.  if so, reuse the original edi_message
+    my $message = $e->search_acq_edi_message({
+        purchase_order => $po_id,
+        message_type => 'ORDERS', 
+        status => 'retry'
+    })->[0];
+
+    if (!$message) {
+        $message = Fieldmapper::acq::edi_message->new;
+        $message->create_time('NOW');
+        $message->purchase_order($po_id);
+        $message->message_type('ORDERS');
+        $message->isnew(1);
+    }
+
+    $message->edi($edi);
+
+    $e->xact_begin;
+    if ($message->isnew) {
+        unless($e->create_acq_edi_message($message)) {
+            announce('error', 
+                "Error creating acq.edi_message for PO $po_id: ".$e->die_event);
+            next;
+        }
+    } else {
+        unless($e->update_acq_edi_message($message)) {
+            announce('error', 
+                "Error updating acq.edi_message for PO $po_id: ".$e->die_event);
+            next;
+        }
+    }
+    $e->xact_commit;
+
+    my $po = $e->retrieve_acq_purchase_order([
+        $po_id, {
+            flesh => 2,
+            flesh_fields => {
+                acqpo  => ['provider'],
+                acqpro => ['edi_default'],
+            }
+        }
+    ]);
+
+    if (!$po->provider->edi_default) {
+        # Caller has provided a PO ID for a provider that does not
+        # support EDI.  
+        announce('error', "Cannot send EDI for PO $po_id, because the ".
+            "provider (".$po->provider->id.") is not configured to use EDI");
+        next;
+    }
+
+    my $res = OpenILS::Application::Acq::EDI->send_core(
+        $po->provider->edi_default, [$message->id]);
+
+    if (my $out = $res->[0]) {
+        announce('info', "message ".$message->id." status: ".$out->status);
+    } else {
+        announce('error', "send_core failed for message ".$message->id);
+    }
+}
+
+

commit 20d7df2b54f3d7e9539da73a625b0dbaae705e0c
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed May 25 17:40:17 2016 -0400

    LP#1373690 Attribute-based EDI generator
    
    New Perl module Utils::EDIWriter for buliding EDI ORDERS messages.
    
    Vendor-specific toggles live in new database tables (acq.edi_attr,
    acq.edi_attr_set, acq.edi_attr_set_map).
    
    The combination of these 2 replaces the current JEDI Action/Trigger
    template with toggle embedded in the template.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index d075d35..7f6e192 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -9213,10 +9213,12 @@ SELECT  usr,
 			<field name="in_dir"        reporter:datatype="text"      reporter:label="Incoming Directory"/>
 			<field name="vendacct"      reporter:datatype="text"      reporter:label="Vendor Account Number"/>
 			<field name="vendcode"      reporter:datatype="text"      reporter:label="Vendor Assigned Code"/>
+			<field name="attr_set"      reporter:datatype="link"      reporter:label="EDI Attribute Set"/>
 		</fields>
 		<links>
 			<link field="provider" reltype="has_a" key="id" map="" class="acqpro"/>
 			<link field="owner" reltype="has_a" key="id" map="" class="aou"/>
+			<link field="attr_set" reltype="has_a" key="id" map="" class="aeas"/>
 		</links>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
             <actions>
@@ -9236,6 +9238,65 @@ SELECT  usr,
         </permacrud>
 	</class>
 
+	<class id="aea" controller="open-ils.cstore open-ils.pcrud" 
+		oils_obj:fieldmapper="acq::edi_attr" 
+		oils_persist:tablename="acq.edi_attr" reporter:label="EDI Attribute">
+		<fields oils_persist:primary="key">
+			<field name="key"   reporter:datatype="text" reporter:label="Key" reporter:selector="label"/>
+			<field name="label" reporter:datatype="text" reporter:label="Label"/>
+		</fields>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="ADMIN_PROVIDER" global_required="true"/>
+                <retrieve/>
+                <update permission="ADMIN_PROVIDER" global_required="true"/>
+                <delete permission="ADMIN_PROVIDER" global_required="true"/>
+            </actions>
+        </permacrud>
+	</class>
+	<class id="aeas" controller="open-ils.cstore open-ils.pcrud" 
+		oils_obj:fieldmapper="acq::edi_attr_set" 
+		oils_persist:tablename="acq.edi_attr_set" reporter:label="EDI Attribute Set">
+		<fields oils_persist:primary="id" oils_persist:sequence="acq.edi_attr_set_id_seq">
+			<field name="id"    reporter:datatype="id"   reporter:label="ID" reporter:selector="label"/>
+			<field name="label" reporter:datatype="text" reporter:label="Label"/>
+			<field name="attr_maps" reporter:datatype="link" oils_persist:virtual="true" reporter:label="Mapped EDI Attributes"/>
+		</fields>
+		<links>
+			<link field="attr_maps" reltype="has_many" key="attr_set" map="" class="aeasm"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="ADMIN_PROVIDER" global_required="true"/>
+                <retrieve/>
+                <update permission="ADMIN_PROVIDER" global_required="true"/>
+                <delete permission="ADMIN_PROVIDER" global_required="true"/>
+            </actions>
+        </permacrud>
+	</class>
+	<class id="aeasm" controller="open-ils.cstore open-ils.pcrud" 
+		oils_obj:fieldmapper="acq::edi_attr_set_map" 
+		oils_persist:tablename="acq.edi_attr_set_map" reporter:label="EDI Attribute Set Map">
+		<fields oils_persist:primary="id" oils_persist:sequence="acq.edi_attr_set_map_id_seq">
+			<field name="id"       reporter:datatype="id"   reporter:label="ID" reporter:selector="label"/>
+			<field name="attr_set" reporter:datatype="link" reporter:label="Attribute Set"/>
+			<field name="attr"     reporter:datatype="link" reporter:label="Attribute"/>
+		</fields>
+		<links>
+			<link field="attr_set" reltype="has_a" key="id" map="" class="aeas"/>
+			<link field="attr" reltype="has_a" key="id" map="" class="aea"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="ADMIN_PROVIDER" global_required="true"/>
+                <retrieve/>
+                <update permission="ADMIN_PROVIDER" global_required="true"/>
+                <delete permission="ADMIN_PROVIDER" global_required="true"/>
+            </actions>
+        </permacrud>
+	</class>
+
+
 	<class id="acqedim" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="acq::edi_message" oils_persist:tablename="acq.edi_message" reporter:label="EDI Message">
 		<fields oils_persist:primary="id" oils_persist:sequence="acq.edi_message_id_seq">
 			<field name="id"               reporter:datatype="id"        reporter:label="EDI Message ID"/>
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Utils/EDIWriter.pm b/Open-ILS/src/perlmods/lib/OpenILS/Utils/EDIWriter.pm
new file mode 100644
index 0000000..6d825f1
--- /dev/null
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Utils/EDIWriter.pm
@@ -0,0 +1,587 @@
+# ---------------------------------------------------------------
+# Copyright (C) 2016 King County Library System
+# Author: Bill Erickson <berickxx at gmail.com>
+#
+# Copied heavily from Application/Trigger/Reactor.pm
+#
+# 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.
+# ---------------------------------------------------------------
+package OpenILS::Utils::EDIWriter;
+use strict; use warnings;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Application::AppUtils;
+use DateTime;
+my $U = 'OpenILS::Application::AppUtils';
+
+sub new {
+    my ($class, $args) = @_;
+    $args ||= {};
+    return bless($args, $class);
+}
+
+# Returns EDI string on success, undef on error.
+sub write {
+    my ($self, $po_id, $msg_type) = @_;
+    $msg_type ||= 'order';
+
+    my $po = $self->get_po($po_id);
+    return undef unless $po;
+
+    $self->compile_po($po);
+    return undef unless $self->{compiled};
+
+    my $edi = $self->build_order_edi if $msg_type eq 'order';
+
+    # remove the newlines unless we are pretty printing
+    $edi =~ s/\n//g unless $self->{pretty};
+
+    return $edi;
+}
+
+sub get_po {
+    my ($self, $po_id) = @_;
+    return new_editor()->retrieve_acq_purchase_order([
+        $po_id, {
+            flesh => 5,
+            flesh_fields => {
+                acqpo   => [qw/lineitems ordering_agency provider/],
+                acqpro  => [qw/edi_default/],
+                acqedi  => [qw/attr_set/],
+                aeas    => [qw/attr_maps/],
+                jub     => [qw/lineitem_details lineitem_notes attributes/],
+                acqlid  => [qw/owning_lib location fund eg_copy_id/],
+                acp     => [qw/location call_number/],
+                aou     => [qw/mailing_address/]
+            }
+        }
+    ]);
+}
+
+sub escape_edi {
+    my ($self, $value) = @_;
+    return '' if (not defined $value || ref($value));
+
+    # Typical vendors dealing with EDIFACT (or is the problem with
+    # our EDI translator itself?) would seem not to want
+    # any characters outside the ASCII range, so trash them.
+    $value =~ s/[^[:ascii:]]//g;
+
+    # What the heck, get rid of [ ] too (although I couldn't get them
+    # to cause any problems for me, problems have been reported. See
+    # LP #812593).
+    $value =~ s/[\[\]]//g;
+
+    # Characters [? + ' \ : <newline>] are all potentially problematic for 
+    # EDI messages, regardless of their position in the string.
+    # Safest to simply remove them.
+    $value =~ s/[\\\?\+':]//g;
+
+    # Replace newlines with spaces.
+    $value =~ s/\n/ /g;
+
+    return $value;
+}
+
+# Returns an EDI-escaped version of the requested lineitem attribute
+# value.  If $attr_type is not set, the first attribute found matching 
+# the requested $attr_name will be used.
+sub get_li_attr {
+    my ($self, $li, $attr_name, $attr_type) = @_;
+
+    for my $attr (@{$li->attributes}) {
+        next unless $attr->attr_name eq $attr_name;
+        next if $attr_type && $attr->attr_type ne $attr_type;
+        return $self->escape_edi($attr->attr_value);
+    }
+
+    return '';
+}
+
+# Generates a HASH version of the PO with all of the data necessary
+# to generate an EDI message from the PO.
+sub compile_po {
+    my ($self, $po) = @_;
+
+    # Cannot generate EDI if the PO has no linked EDI account.
+    return undef unless $po->provider->edi_default;
+
+    my %compiled = (
+        po_id => $po->id,
+        po_name => $self->escape_edi($po->name),
+        provider_id => $po->provider->id,
+        vendor_san => $po->provider->san || '',
+        org_unit_san => $po->ordering_agency->mailing_address->san || '',
+        currency_type => $po->provider->currency_type,
+        edi_attrs => {},
+        lineitems => []
+    );
+
+    $self->{compiled} = \%compiled;
+    
+    if ($po->provider->edi_default->attr_set) {
+        $compiled{edi_attrs}{$_->attr} = 1 
+            for @{$po->provider->edi_default->attr_set->attr_maps}
+    }
+
+    $compiled{buyer_code} = 
+        $compiled{edi_attrs}->{BUYER_ID_INCLUDE_VENDCODE} ? # B&T
+        $compiled{vendor_san}.' '.$po->provider->edi_default->vendcode :
+        $po->provider->edi_default->vendacct;
+
+    push(@{$compiled{lineitems}}, 
+        $self->compile_li($_)) for @{$po->lineitems};
+
+    return \%compiled;
+}
+
+# Translate a lineitem order identifier attribute into an 
+# EDI ID value and ID qualifier.
+sub set_li_order_ident {
+    my ($self, $li, $li_hash) = @_;
+
+    my $idqual = 'EN'; # ISBN13
+    my $idval = '';
+
+    if ($self->{compiled}->{edi_attr}->{LINEITEM_IDENT_VENDOR_NUMBER}) {
+        # See if we have a vendor-specific lineitem identifier value
+        $idval = $self->get_li_attr($li, 'vendor_num');
+    }
+
+    if (!$idval) {
+
+        my $attr = $self->get_li_order_ident_attr($li->attributes);
+
+        if ($attr) {
+            my $name = $attr->attr_name;
+            $idval = $attr->attr_value;
+
+            if ($name eq 'isbn' && length($idval) != 13) {
+                $idqual = 'IB';
+            } elsif ($name eq 'issn') {
+                $idqual = 'IS';
+            }
+        } else {
+            $idqual = 'IN';
+            $idval = $li->id;
+        }
+    }
+
+    $li_hash->{idqual} = $idqual;
+    $li_hash->{idval} = $idval;
+}
+
+# Find the acq.lineitem_attr object that represents the identifier 
+# for a lineitem.
+sub get_li_order_ident_attr {
+    my ($self, $attrs) = @_;
+
+    # preferred identifier
+    my ($attr) =  grep { $U->is_true($_->order_ident) } @$attrs;
+    return $attr if $attr;
+
+    # note we're not using get_li_attr, since we need the 
+    # attr object and not just the attr value
+
+    # isbn-13
+    ($attr) = grep { 
+        $_->attr_name eq 'isbn' and 
+        $_->attr_type eq 'lineitem_marc_attr_definition' and
+        length($_->attr_value) == 13
+    } @$attrs;
+    return $attr if $attr;
+
+    for my $name (qw/isbn issn upc/) {
+        ($attr) = grep { 
+            $_->attr_name eq $name and 
+            $_->attr_type eq 'lineitem_marc_attr_definition'
+        } @$attrs;
+        return $attr if $attr;
+    }
+
+    # any 'identifier' attr
+    return (grep { $_->attr_name eq 'identifier' } @$attrs)[0];
+}
+
+# Collect FTX notes and chop them into FTX-compatible values.
+sub get_li_ftx {
+    my ($self, $li) = @_;
+
+    # all vendor-public, non-empty lineitem notes
+    my @notes = 
+        map {$_->value} 
+        grep { $U->is_true($_->vendor_public) && $_->value } 
+        @{$li->lineitem_notes};
+
+    if ($self->{compiled}->{edi_attr}->{COPY_SPEC_CODES}) {
+        for my $lid (@{$li->lineitem_details}) {
+            push(@notes, $lid->note) 
+                if ($lid->note || '') =~ /spec code [a-zA-Z0-9_]/;
+        }
+    }
+
+    my @trimmed_notes;
+
+    if (!@notes && $self->{compiled}->{edi_attr}->{INCLUDE_EMPTY_LI_NOTE}) {
+        # lineitem has no notes.  Add a blank note if needed.
+        push(@trimmed_notes, '');
+
+    } else {
+        # EDI FTX fields have a max length of 512
+        # While we're in here, EDI-escape the note values
+        for my $note (@notes) {
+            $note = $self->escape_edi($note);
+            my @parts = ($note =~ m/.{1,512}/g);
+            push(@trimmed_notes, @parts);
+        }
+    }
+
+    return \@trimmed_notes;
+}
+
+sub compile_li {
+    my ($self, $li) = @_;
+
+    my $li_hash = {
+        id => $li->id,
+        quantity => scalar(@{$li->lineitem_details}),
+        estimated_unit_price => $li->estimated_unit_price || '0.00',
+        notes => $self->get_li_ftx($li),
+        copies => []
+    };
+
+    $self->set_li_order_ident($li, $li_hash);
+
+    for my $name (qw/title author edition pubdate publisher pagination/) {
+        $li_hash->{$name} = $self->get_li_attr($li, $name);
+    }
+
+    $self->compile_copies($li, $li_hash);
+
+    return $li_hash;
+}
+
+sub compile_copies { 
+    my ($self, $li, $li_hash) = @_;
+
+    # does this EDI account want copy data?
+    return unless $self->{compiled}->{edi_attrs}->{INCLUDE_COPIES};
+
+    for my $copy (@{$li->lineitem_details}) {
+        $self->compile_copy($li, $li_hash, $copy);
+    }
+}
+
+sub compile_copy {
+    my ($self, $li, $li_hash, $copy) = @_;
+
+    my $fund = $copy->fund ? $copy->fund->code : '';
+    my $item_type = $copy->circ_modifier || '';
+    my $call_number = $copy->cn_label || '';
+    my $owning_lib = $copy->owning_lib ? $copy->owning_lib->shortname : '';
+    my $location = $copy->location ? $copy->location->name : '';
+    my $collection_code = $copy->collection_code || '';
+    my $barcode = $copy->barcode || '';
+
+   
+    # When an ACQ copy links to a real copy (acp), treat the real
+    # copy as authoritative for certain fields.
+    my $acp = $copy->eg_copy_id;
+    if ($acp) {
+        $item_type = $acp->circ_modifier || '';
+        $call_number = $acp->call_number->label;
+        $location = $acp->location->name;
+    }
+
+    my $found_match = 0;
+
+    # Collapse like copies into groups with a quantity value.
+    # INCLUDE_COPY_ID implies one GIR row per copy, no collapsing.
+    if (!$self->{compiled}->{edi_attrs}->{INCLUDE_COPY_ID}) {
+        
+        for my $e_copy (@{$li_hash->{copies}}) {
+            if (
+                ($fund eq $e_copy->{fund}) &&
+                ($item_type eq $e_copy->{item_type}) &&
+                ($call_number eq $e_copy->{call_number}) &&
+                ($owning_lib eq $e_copy->{owning_lib}) &&
+                ($location eq $e_copy->{location}) &&
+                ($barcode eq $e_copy->{barcode}) &&
+                ($collection_code eq $e_copy->{collection_code})
+            ) {
+                $e_copy->{quantity}++;
+                $found_match = 1;
+                last;
+            }
+        }
+    }
+
+    return if $found_match; # nothing left to do.
+
+    # No matching copy found.  Add it as a new copy to the lineitem
+    # copies array.
+
+    push(@{$li_hash->{copies}}, {
+        fund => $self->escape_edi($fund),
+        item_type => $self->escape_edi($item_type),
+        call_number => $self->escape_edi($call_number),
+        owning_lib => $self->escape_edi($owning_lib),
+        location => $self->escape_edi($location),
+        barcode => $self->escape_edi($barcode),
+        collection_code => $self->escape_edi($collection_code),
+        copy_id => $copy->id, # for INCLUDE_COPY_ID
+        quantity => 1
+    });
+}
+
+# IMD fields are limited to 70 chars per value.  Any values longer
+# should be carried via repeating IMD fields.
+# IMD fields should only display the +::: when a value is present
+sub IMD {
+    my ($self, $code, $value) = @_;
+    if ($value) {
+        my $s = '';
+        for my $part ($value =~ m/.{1,70}/g) {
+            $s .= "IMD+F+$code+:::$part'\n"; }
+        return $s;
+
+    } else {
+        return "IMD+F+$code'\n"
+    }
+}
+
+# EDI Segments: --
+# UNA
+# UNB
+# UNH
+# BGM
+# DTM
+# NAD+BY
+# NAD+SU...::31B
+# NAD+SU...::92
+# CUX
+# <lineitems and copies>
+# UNS
+# CNT
+# UNT
+# UNZ
+sub build_order_edi {
+    my ($self) = @_;
+    my %c = %{$self->{compiled}};
+    my $date = DateTime->now->strftime("%Y%m%d");
+    my $datetime = DateTime->now->strftime("%y%m%d:%H%M");
+    my @lis = @{$c{lineitems}};
+
+    # EDI header
+    my $edi = <<EDI;
+UNA:+.? '
+UNB+UNOB:3+$c{org_unit_san}:31B+$c{vendor_san}:31B+$datetime+1'
+UNH+1+ORDERS:D:96A:UN'
+BGM+220+$c{po_id}+9'
+DTM+137:$date:102'
+EDI
+
+    $edi .= "NAD+BY+$c{org_unit_san}::31B'\n"
+        unless $self->{compiled}->{edi_attrs}->{BUYER_ID_INCLUDE_VENDCODE};
+
+    $edi .= <<EDI;
+NAD+BY+$c{buyer_code}::91'
+NAD+SU+$c{vendor_san}::31B'
+NAD+SU+$c{provider_id}::92'
+CUX+2:$c{currency_type}:9'
+EDI
+
+    # EDI lineitem segments
+    $edi .= $self->build_lineitem_segments($_) for @lis;
+
+    my $li_count = scalar(@lis);
+
+    # Count the number of segments in the EDI message by counting the
+    # number of newlines.  Add to count for lines below, not including
+    # the UNZ segment.
+    my $segments = $edi =~ tr/\n//;
+    $segments += 1; # UNS, CNT, UNT, but not UNA or UNB
+
+    # EDI Trailer
+    $edi .= <<EDI;
+UNS+S'
+CNT+2:$li_count'
+UNT+$segments+1'
+UNZ+1+1'
+EDI
+
+    return $edi;
+}
+
+# EDI Segments: --
+# LIN
+# PIA+5
+# IMD+F+BTI
+# IMD+F+BPD
+# IMD+F+BPU
+# IMD+F+BAU
+# IMD+F+BEN
+# IMD+F+BPH
+# QTY+21
+# FTX+LIN
+# PRI+AAB
+# RFF+LI
+sub build_lineitem_segments {
+    my ($self, $li_hash) = @_;
+    my %c = %{$self->{compiled}};
+
+    my $id = $li_hash->{id};
+    my $idval = $li_hash->{idval};
+    my $idqual = $li_hash->{idqual};
+    my $quantity = $li_hash->{quantity};
+    my $price = $li_hash->{estimated_unit_price};
+
+    # Line item identifier segments
+    my $edi = "LIN+$id++$idval:$idqual'\n";
+    $edi .= "PIA+5+$idval:$idqual'\n";
+
+    $edi .= $self->IMD('BTI', $li_hash->{title});
+    $edi .= $self->IMD('BPU', $li_hash->{publisher});
+    $edi .= $self->IMD('BPD', $li_hash->{pubdate});
+
+    $edi .= $self->IMD('BEN', $li_hash->{edition})
+        if $c{edi_attrs}->{INCLUDE_BIB_EDITION};
+
+    $edi .= $self->IMD('BAU', $li_hash->{author})
+        if $c{edi_attrs}->{INCLUDE_BIB_AUTHOR};
+
+    $edi .= $self->IMD('BPH', $li_hash->{pagination})
+        if $c{edi_attrs}->{INCLUDE_BIB_PAGINATION};
+
+    $edi .= "QTY+21:$quantity'\n";
+
+    $edi .= $self->build_gir_segments($li_hash);
+
+    for my $note (@{$li_hash->{notes}}) {
+        if ($note) {
+            $edi .= "FTX+LIN+1+$note'\n"
+        } else {
+            $edi .= "FTX+LIN+1'\n"
+        }
+    }
+
+    $edi .= "PRI+AAB:$price'\n";
+
+    # Standard RFF
+    my $rff = "$c{po_id}/$id";
+
+    if ($c{edi_attrs}->{LINEITEM_REF_ID_ONLY}) {
+        # RFF with lineitem ID only (typically B&T)
+        $rff = $id;
+    } elsif ($c{edi_attrs}->{INCLUDE_PO_NAME}) {
+        # RFF with PO name instead of PO ID
+        $rff = "$c{po_name}/$id";
+    }
+
+    $edi .= "RFF+LI:$rff'\n";
+
+    return $edi;
+}
+
+
+# Map of GIR segment codes, copy field names, inclusion attributes,
+# and include-if-empty attributes for encoding copy data.
+my @gir_fields = (
+    {   code => 'LLO', 
+        field => 'owning_lib', 
+        attr => 'INCLUDE_OWNING_LIB'},
+    {   code => 'LSQ', 
+        field => 'collection_code', 
+        attr => 'INCLUDE_COLLECTION_CODE', 
+        empty_attr => 'INCLUDE_EMPTY_COLLECTION_CODE'},
+    {   code => 'LQT', 
+        field => 'quantity', 
+        attr => 'INCLUDE_QUANTITY'},
+    {   code => 'LCO',
+        field => 'copy_id',
+        attr => 'INCLUDE_COPY_ID'},
+    {   code => 'LST', 
+        field => 'item_type', 
+        attr => 'INCLUDE_ITEM_TYPE',
+        empty_attr => 'INCLUDE_EMPTY_ITEM_TYPE'},
+    {   code => 'LSM', 
+        field => 'call_number', 
+        attr => 'INCLUDE_CALL_NUMBER', 
+        empty_attr => 'INCLUDE_EMPTY_CALL_NUMBER'},
+    {   code => 'LFN', 
+        field => 'fund', 
+        attr => 'INCLUDE_FUND'},
+    {   code => 'LFH', 
+        field => 'location', 
+        attr => 'INCLUDE_LOCATION',
+        empty_attr => 'INCLUDE_EMPTY_LOCATION'},
+    {   code => 'LAC',
+        field => 'barcode',
+        attr => 'INCLUDE_ITEM_BARCODE'}
+);
+
+# EDI Segments: --
+# GIR
+# Sub-Segments: --
+# LLO
+# LFN
+# LSM
+# LST
+# LSQ
+# LFH
+# LQT
+sub build_gir_segments {
+    my ($self, $li_hash) = @_;
+    my %c = %{$self->{compiled}};
+    my $gir_index = 0;
+    my $edi = '';
+
+    for my $copy (@{$li_hash->{copies}}) {
+        $gir_index++;
+        my $gir_idx_str = sprintf("%03d", $gir_index);
+
+        my $field_count = 0;
+        for my $field (@gir_fields) {
+            next unless $c{edi_attrs}->{$field->{attr}};
+
+            my $val = $copy->{$field->{field}};
+            my $code = $field->{code};
+
+            # include the GIR component if we have a value or this
+            # EDI account is configured to include the empty value
+            next unless $val || $c{edi_attrs}->{$field->{empty_attr} || ''};
+
+            # EDI only allows 5 fields per GIR segment.  When we exceed
+            # 5, finalize the in-process GIR segment and add a new one
+            # as needed.
+            if ($field_count == 5) {
+                $field_count = 0;
+                # Finalize this GIR segment with a ' and newline
+                $edi .= "'\n";
+            }
+
+            $field_count++;
+
+            # Starting a new GIR line for the current copy.
+            $edi .= "GIR+$gir_idx_str" if $field_count == 1;
+
+            # Add the field-specific value
+            $edi .= "+$val:$code";
+        }
+
+        # End the final GIR segment with a ' and newline
+        $edi .= "'\n";
+    }
+
+    return $edi;
+}
+
+1;
+
diff --git a/Open-ILS/src/sql/Pg/200.schema.acq.sql b/Open-ILS/src/sql/Pg/200.schema.acq.sql
index 6391e68..7fa60e3 100644
--- a/Open-ILS/src/sql/Pg/200.schema.acq.sql
+++ b/Open-ILS/src/sql/Pg/200.schema.acq.sql
@@ -739,11 +739,31 @@ CREATE TABLE acq.fiscal_year (
     CONSTRAINT acq_fy_physical_key UNIQUE ( calendar, year_begin )
 );
 
+CREATE TABLE acq.edi_attr (
+    key     TEXT PRIMARY KEY,
+    label   TEXT NOT NULL UNIQUE
+);
+
+CREATE TABLE acq.edi_attr_set (
+    id      SERIAL  PRIMARY KEY,
+    label   TEXT NOT NULL UNIQUE
+);
+
+CREATE TABLE acq.edi_attr_set_map (
+    id          SERIAL  PRIMARY KEY,
+    attr_set    INTEGER NOT NULL REFERENCES acq.edi_attr_set(id) 
+                ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    attr        TEXT NOT NULL REFERENCES acq.edi_attr(key) 
+                ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    CONSTRAINT edi_attr_set_map_attr_once UNIQUE (attr_set, attr)
+);
+
 CREATE TABLE acq.edi_account (      -- similar tables can extend remote_account for other parts of EG
     provider    INT     NOT NULL REFERENCES acq.provider          (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
     in_dir      TEXT,   -- incoming messages dir (probably different than config.remote_account.path, the outgoing dir)
     vendcode    TEXT,
-    vendacct    TEXT
+    vendacct    TEXT,
+    attr_set    INTEGER REFERENCES acq.edi_attr_set(id) -- NULL OK
 ) INHERITS (config.remote_account);
 
 -- We need a UNIQUE constraint here also, to support the FK from acq.provider.edi_default
diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index 391ad5a..8523778 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -17187,3 +17187,150 @@ VALUES (
 );
 
 
+INSERT INTO acq.edi_attr (key, label) VALUES
+    ('INCLUDE_PO_NAME', 
+        oils_i18n_gettext('INCLUDE_PO_NAME', 
+        'Oders Include PO Name', 'aea', 'label')),
+    ('INCLUDE_COPIES', 
+        oils_i18n_gettext('INCLUDE_COPIES', 
+        'Orders Include Copy Data', 'aea', 'label')),
+    ('INCLUDE_FUND', 
+        oils_i18n_gettext('INCLUDE_FUND', 
+        'Orders Include Copy Funds', 'aea', 'label')),
+    ('INCLUDE_CALL_NUMBER', 
+        oils_i18n_gettext('INCLUDE_CALL_NUMBER', 
+        'Orders Include Copy Call Numbers', 'aea', 'label')),
+    ('INCLUDE_ITEM_TYPE', 
+        oils_i18n_gettext('INCLUDE_ITEM_TYPE', 
+        'Orders Include Copy Item Types', 'aea', 'label')),
+    ('INCLUDE_ITEM_BARCODE',
+        oils_i18n_gettext('INCLUDE_ITEM_BARCODE',
+        'Orders Include Copy Barcodes', 'aea', 'label')),
+    ('INCLUDE_LOCATION', 
+        oils_i18n_gettext('INCLUDE_LOCATION', 
+        'Orders Include Copy Locations', 'aea', 'label')),
+    ('INCLUDE_COLLECTION_CODE', 
+        oils_i18n_gettext('INCLUDE_COLLECTION_CODE', 
+        'Orders Include Copy Collection Codes', 'aea', 'label')),
+    ('INCLUDE_OWNING_LIB', 
+        oils_i18n_gettext('INCLUDE_OWNING_LIB', 
+        'Orders Include Copy Owning Library', 'aea', 'label')),
+    ('INCLUDE_QUANTITY', 
+        oils_i18n_gettext('INCLUDE_QUANTITY', 
+        'Orders Include Copy Quantities', 'aea', 'label')),
+    ('INCLUDE_COPY_ID', 
+        oils_i18n_gettext('INCLUDE_COPY_ID', 
+        'Orders Include Copy IDs', 'aea', 'label')),
+    ('BUYER_ID_INCLUDE_VENDCODE', 
+        oils_i18n_gettext('BUYER_ID_INCLUDE_VENDCODE', 
+        'Buyer ID Qualifier Includes Vendcode', 'aea', 'label')),
+    ('INCLUDE_BIB_EDITION', 
+        oils_i18n_gettext('INCLUDE_BIB_EDITION', 
+        'Order Lineitems Include Edition Info', 'aea', 'label')),
+    ('INCLUDE_BIB_AUTHOR', 
+        oils_i18n_gettext('INCLUDE_BIB_AUTHOR', 
+        'Order Lineitems Include Author Info', 'aea', 'label')),
+    ('INCLUDE_BIB_PAGINATION', 
+        oils_i18n_gettext('INCLUDE_BIB_PAGINATION', 
+        'Order Lineitems Include Pagination Info', 'aea', 'label')),
+    ('COPY_SPEC_CODES', 
+        oils_i18n_gettext('COPY_SPEC_CODES', 
+        'Order Lineitem Notes Include Copy Spec Codes', 'aea', 'label')),
+    ('INCLUDE_EMPTY_LI_NOTE', 
+        oils_i18n_gettext('INCLUDE_EMPTY_LI_NOTE', 
+        'Order Lineitem Notes Always Present (Even if Empty)', 'aea', 'label')),
+    ('INCLUDE_EMPTY_CALL_NUMBER', 
+        oils_i18n_gettext('INCLUDE_EMPTY_CALL_NUMBER', 
+        'Order Copies Always Include Call Number (Even if Empty)', 'aea', 'label')),
+    ('INCLUDE_EMPTY_ITEM_TYPE', 
+        oils_i18n_gettext('INCLUDE_EMPTY_ITEM_TYPE', 
+        'Order Copies Always Include Item Type (Even if Empty)', 'aea', 'label')),
+    ('INCLUDE_EMPTY_LOCATION', 
+        oils_i18n_gettext('INCLUDE_EMPTY_LOCATION', 
+        'Order Copies Always Include Location (Even if Empty)', 'aea', 'label')),
+    ('INCLUDE_EMPTY_COLLECTION_CODE', 
+        oils_i18n_gettext('INCLUDE_EMPTY_COLLECTION_CODE', 
+        'Order Copies Always Include Collection Code (Even if Empty)', 'aea', 'label')),
+    ('LINEITEM_IDENT_VENDOR_NUMBER',
+        oils_i18n_gettext('LINEITEM_IDENT_VENDOR_NUMBER',
+        'Lineitem Identifier Fields (LIN/PIA) Use Vendor-Encoded ID Value When Available', 'aea', 'label')),
+    ('LINEITEM_REF_ID_ONLY',
+        oils_i18n_gettext('LINEITEM_REF_ID_ONLY',
+        'Lineitem Reference Feld (RFF) Uses Lineitem ID Only', 'aea', 'label'))
+
+;
+
+INSERT INTO acq.edi_attr_set (id, label) VALUES (1, 'Ingram Default');
+INSERT INTO acq.edi_attr_set (id, label) VALUES (2, 'Baker & Taylor Default');
+INSERT INTO acq.edi_attr_set (id, label) VALUES (3, 'Brodart Default');
+INSERT INTO acq.edi_attr_set (id, label) VALUES (4, 'Midwest Tape Default');
+INSERT INTO acq.edi_attr_set (id, label) VALUES (5, 'ULS Default');
+INSERT INTO acq.edi_attr_set (id, label) VALUES (6, 'Recorded Books Default');
+
+-- carve out space for mucho defaults
+SELECT SETVAL('acq.edi_attr_set_id_seq'::TEXT, 1000);
+
+INSERT INTO acq.edi_attr_set_map (attr_set, attr) VALUES
+
+    -- Ingram
+    (1, 'INCLUDE_PO_NAME'),
+    (1, 'INCLUDE_COPIES'),
+    (1, 'INCLUDE_ITEM_TYPE'),
+    (1, 'INCLUDE_COLLECTION_CODE'),
+    (1, 'INCLUDE_OWNING_LIB'),
+    (1, 'INCLUDE_QUANTITY'),
+    (1, 'INCLUDE_BIB_PAGINATION'),
+
+    -- B&T
+    (2, 'INCLUDE_COPIES'),
+    (2, 'INCLUDE_ITEM_TYPE'),
+    (2, 'INCLUDE_COLLECTION_CODE'),
+    (2, 'INCLUDE_CALL_NUMBER'),
+    (2, 'INCLUDE_OWNING_LIB'),
+    (2, 'INCLUDE_QUANTITY'),
+    (2, 'INCLUDE_BIB_PAGINATION'),
+    (2, 'BUYER_ID_INCLUDE_VENDCODE'),
+    (2, 'INCLUDE_EMPTY_LI_NOTE'),
+    (2, 'INCLUDE_EMPTY_CALL_NUMBER'),
+    (2, 'INCLUDE_EMPTY_ITEM_TYPE'),
+    (2, 'INCLUDE_EMPTY_COLLECTION_CODE'),
+    (2, 'INCLUDE_EMPTY_LOCATION'),
+    (2, 'LINEITEM_IDENT_VENDOR_NUMBER'),
+    (2, 'LINEITEM_REF_ID_ONLY'),
+
+    -- Brodart
+    (3, 'INCLUDE_COPIES'),
+    (3, 'INCLUDE_FUND'),
+    (3, 'INCLUDE_ITEM_TYPE'),
+    (3, 'INCLUDE_COLLECTION_CODE'),
+    (3, 'INCLUDE_OWNING_LIB'),
+    (3, 'INCLUDE_QUANTITY'),
+    (3, 'INCLUDE_BIB_PAGINATION'),
+    (3, 'COPY_SPEC_CODES'),
+
+    -- Midwest
+    (4, 'INCLUDE_COPIES'),
+    (4, 'INCLUDE_FUND'),
+    (4, 'INCLUDE_OWNING_LIB'),
+    (4, 'INCLUDE_QUANTITY'),
+    (4, 'INCLUDE_BIB_PAGINATION'),
+
+    -- ULS
+    (5, 'INCLUDE_COPIES'),
+    (5, 'INCLUDE_ITEM_TYPE'),
+    (5, 'INCLUDE_COLLECTION_CODE'),
+    (5, 'INCLUDE_OWNING_LIB'),
+    (5, 'INCLUDE_QUANTITY'),
+    (5, 'INCLUDE_BIB_AUTHOR'),
+    (5, 'INCLUDE_BIB_EDITION'),
+    (5, 'INCLUDE_EMPTY_LI_NOTE'),
+
+    -- Recorded Books
+    (6, 'INCLUDE_COPIES'),
+    (6, 'INCLUDE_ITEM_TYPE'),
+    (6, 'INCLUDE_COLLECTION_CODE'),
+    (6, 'INCLUDE_OWNING_LIB'),
+    (6, 'INCLUDE_QUANTITY'),
+    (6, 'INCLUDE_BIB_PAGINATION')
+;
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.edi_attr_set.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.edi_attr_set.sql
new file mode 100644
index 0000000..be1aa23
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.edi_attr_set.sql
@@ -0,0 +1,31 @@
+BEGIN;
+
+-- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+CREATE TABLE acq.edi_attr (
+    key     TEXT PRIMARY KEY,
+    label   TEXT NOT NULL UNIQUE
+);
+
+CREATE TABLE acq.edi_attr_set (
+    id      SERIAL  PRIMARY KEY,
+    label   TEXT NOT NULL UNIQUE
+);
+
+CREATE TABLE acq.edi_attr_set_map (
+    id          SERIAL  PRIMARY KEY,
+    attr_set    INTEGER NOT NULL REFERENCES acq.edi_attr_set(id) 
+                ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    attr        TEXT NOT NULL REFERENCES acq.edi_attr(key) 
+                ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    CONSTRAINT edi_attr_set_map_attr_once UNIQUE (attr_set, attr)
+);
+
+-- An attr_set is not strictly required, since some edi_accounts/vendors 
+-- may not need to apply any attributes.
+ALTER TABLE acq.edi_account ADD COLUMN attr_set 
+    INTEGER REFERENCES acq.edi_attr_set(id);
+
+COMMIT;
+
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.edi_attr_set.sql b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.edi_attr_set.sql
new file mode 100644
index 0000000..7bb42f6
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.edi_attr_set.sql
@@ -0,0 +1,155 @@
+BEGIN;
+
+-- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+INSERT INTO acq.edi_attr (key, label) VALUES
+    ('INCLUDE_PO_NAME', 
+        oils_i18n_gettext('INCLUDE_PO_NAME', 
+        'Oders Include PO Name', 'aea', 'label')),
+    ('INCLUDE_COPIES', 
+        oils_i18n_gettext('INCLUDE_COPIES', 
+        'Orders Include Copy Data', 'aea', 'label')),
+    ('INCLUDE_FUND', 
+        oils_i18n_gettext('INCLUDE_FUND', 
+        'Orders Include Copy Funds', 'aea', 'label')),
+    ('INCLUDE_CALL_NUMBER', 
+        oils_i18n_gettext('INCLUDE_CALL_NUMBER', 
+        'Orders Include Copy Call Numbers', 'aea', 'label')),
+    ('INCLUDE_ITEM_TYPE', 
+        oils_i18n_gettext('INCLUDE_ITEM_TYPE', 
+        'Orders Include Copy Item Types', 'aea', 'label')),
+    ('INCLUDE_ITEM_BARCODE',
+        oils_i18n_gettext('INCLUDE_ITEM_BARCODE',
+        'Orders Include Copy Barcodes', 'aea', 'label')),
+    ('INCLUDE_LOCATION', 
+        oils_i18n_gettext('INCLUDE_LOCATION', 
+        'Orders Include Copy Locations', 'aea', 'label')),
+    ('INCLUDE_COLLECTION_CODE', 
+        oils_i18n_gettext('INCLUDE_COLLECTION_CODE', 
+        'Orders Include Copy Collection Codes', 'aea', 'label')),
+    ('INCLUDE_OWNING_LIB', 
+        oils_i18n_gettext('INCLUDE_OWNING_LIB', 
+        'Orders Include Copy Owning Library', 'aea', 'label')),
+    ('INCLUDE_QUANTITY', 
+        oils_i18n_gettext('INCLUDE_QUANTITY', 
+        'Orders Include Copy Quantities', 'aea', 'label')),
+    ('INCLUDE_COPY_ID', 
+        oils_i18n_gettext('INCLUDE_COPY_ID', 
+        'Orders Include Copy IDs', 'aea', 'label')),
+    ('BUYER_ID_INCLUDE_VENDCODE', 
+        oils_i18n_gettext('BUYER_ID_INCLUDE_VENDCODE', 
+        'Buyer ID Qualifier Includes Vendcode', 'aea', 'label')),
+    ('INCLUDE_BIB_EDITION', 
+        oils_i18n_gettext('INCLUDE_BIB_EDITION', 
+        'Order Lineitems Include Edition Info', 'aea', 'label')),
+    ('INCLUDE_BIB_AUTHOR', 
+        oils_i18n_gettext('INCLUDE_BIB_AUTHOR', 
+        'Order Lineitems Include Author Info', 'aea', 'label')),
+    ('INCLUDE_BIB_PAGINATION', 
+        oils_i18n_gettext('INCLUDE_BIB_PAGINATION', 
+        'Order Lineitems Include Pagination Info', 'aea', 'label')),
+    ('COPY_SPEC_CODES', 
+        oils_i18n_gettext('COPY_SPEC_CODES', 
+        'Order Lineitem Notes Include Copy Spec Codes', 'aea', 'label')),
+    ('INCLUDE_EMPTY_LI_NOTE', 
+        oils_i18n_gettext('INCLUDE_EMPTY_LI_NOTE', 
+        'Order Lineitem Notes Always Present (Even if Empty)', 'aea', 'label')),
+    ('INCLUDE_EMPTY_CALL_NUMBER', 
+        oils_i18n_gettext('INCLUDE_EMPTY_CALL_NUMBER', 
+        'Order Copies Always Include Call Number (Even if Empty)', 'aea', 'label')),
+    ('INCLUDE_EMPTY_ITEM_TYPE', 
+        oils_i18n_gettext('INCLUDE_EMPTY_ITEM_TYPE', 
+        'Order Copies Always Include Item Type (Even if Empty)', 'aea', 'label')),
+    ('INCLUDE_EMPTY_LOCATION', 
+        oils_i18n_gettext('INCLUDE_EMPTY_LOCATION', 
+        'Order Copies Always Include Location (Even if Empty)', 'aea', 'label')),
+    ('INCLUDE_EMPTY_COLLECTION_CODE', 
+        oils_i18n_gettext('INCLUDE_EMPTY_COLLECTION_CODE', 
+        'Order Copies Always Include Collection Code (Even if Empty)', 'aea', 'label')),
+    ('LINEITEM_IDENT_VENDOR_NUMBER',
+        oils_i18n_gettext('LINEITEM_IDENT_VENDOR_NUMBER',
+        'Lineitem Identifier Fields (LIN/PIA) Use Vendor-Encoded ID Value When Available', 'aea', 'label')),
+    ('LINEITEM_REF_ID_ONLY',
+        oils_i18n_gettext('LINEITEM_REF_ID_ONLY',
+        'Lineitem Reference Feld (RFF) Uses Lineitem ID Only', 'aea', 'label'))
+
+;
+
+INSERT INTO acq.edi_attr_set (id, label) VALUES (1, 'Ingram Default');
+INSERT INTO acq.edi_attr_set (id, label) VALUES (2, 'Baker & Taylor Default');
+INSERT INTO acq.edi_attr_set (id, label) VALUES (3, 'Brodart Default');
+INSERT INTO acq.edi_attr_set (id, label) VALUES (4, 'Midwest Tape Default');
+INSERT INTO acq.edi_attr_set (id, label) VALUES (5, 'ULS Default');
+INSERT INTO acq.edi_attr_set (id, label) VALUES (6, 'Recorded Books Default');
+
+-- carve out space for mucho defaults
+SELECT SETVAL('acq.edi_attr_set_id_seq'::TEXT, 1000);
+
+INSERT INTO acq.edi_attr_set_map (attr_set, attr) VALUES
+
+    -- Ingram
+    (1, 'INCLUDE_PO_NAME'),
+    (1, 'INCLUDE_COPIES'),
+    (1, 'INCLUDE_ITEM_TYPE'),
+    (1, 'INCLUDE_COLLECTION_CODE'),
+    (1, 'INCLUDE_OWNING_LIB'),
+    (1, 'INCLUDE_QUANTITY'),
+    (1, 'INCLUDE_BIB_PAGINATION'),
+
+    -- B&T
+    (2, 'INCLUDE_COPIES'),
+    (2, 'INCLUDE_ITEM_TYPE'),
+    (2, 'INCLUDE_COLLECTION_CODE'),
+    (2, 'INCLUDE_CALL_NUMBER'),
+    (2, 'INCLUDE_OWNING_LIB'),
+    (2, 'INCLUDE_QUANTITY'),
+    (2, 'INCLUDE_BIB_PAGINATION'),
+    (2, 'BUYER_ID_INCLUDE_VENDCODE'),
+    (2, 'INCLUDE_EMPTY_LI_NOTE'),
+    (2, 'INCLUDE_EMPTY_CALL_NUMBER'),
+    (2, 'INCLUDE_EMPTY_ITEM_TYPE'),
+    (2, 'INCLUDE_EMPTY_COLLECTION_CODE'),
+    (2, 'INCLUDE_EMPTY_LOCATION'),
+    (2, 'LINEITEM_IDENT_VENDOR_NUMBER'),
+    (2, 'LINEITEM_REF_ID_ONLY'),
+
+    -- Brodart
+    (3, 'INCLUDE_COPIES'),
+    (3, 'INCLUDE_FUND'),
+    (3, 'INCLUDE_ITEM_TYPE'),
+    (3, 'INCLUDE_COLLECTION_CODE'),
+    (3, 'INCLUDE_OWNING_LIB'),
+    (3, 'INCLUDE_QUANTITY'),
+    (3, 'INCLUDE_BIB_PAGINATION'),
+    (3, 'COPY_SPEC_CODES'),
+
+    -- Midwest
+    (4, 'INCLUDE_COPIES'),
+    (4, 'INCLUDE_FUND'),
+    (4, 'INCLUDE_OWNING_LIB'),
+    (4, 'INCLUDE_QUANTITY'),
+    (4, 'INCLUDE_BIB_PAGINATION'),
+
+    -- ULS
+    (5, 'INCLUDE_COPIES'),
+    (5, 'INCLUDE_ITEM_TYPE'),
+    (5, 'INCLUDE_COLLECTION_CODE'),
+    (5, 'INCLUDE_OWNING_LIB'),
+    (5, 'INCLUDE_QUANTITY'),
+    (5, 'INCLUDE_BIB_AUTHOR'),
+    (5, 'INCLUDE_BIB_EDITION'),
+    (5, 'INCLUDE_EMPTY_LI_NOTE'),
+
+    -- Recorded Books
+    (6, 'INCLUDE_COPIES'),
+    (6, 'INCLUDE_ITEM_TYPE'),
+    (6, 'INCLUDE_COLLECTION_CODE'),
+    (6, 'INCLUDE_OWNING_LIB'),
+    (6, 'INCLUDE_QUANTITY'),
+    (6, 'INCLUDE_BIB_PAGINATION')
+;
+
+
+COMMIT;
+
+
diff --git a/Open-ILS/src/support-scripts/test-scripts/edi_writer.pl b/Open-ILS/src/support-scripts/test-scripts/edi_writer.pl
new file mode 100755
index 0000000..075f2f1
--- /dev/null
+++ b/Open-ILS/src/support-scripts/test-scripts/edi_writer.pl
@@ -0,0 +1,25 @@
+#!/usr/bin/perl
+use strict; use warnings;
+use OpenILS::Utils::EDIWriter;
+require '../oils_header.pl';
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use Getopt::Long;
+
+my $config = '/openils/conf/opensrf_core.xml';
+my $po_id;
+
+GetOptions(
+    'osrf-config' => \$config,
+    'po-id=i' => \$po_id
+);
+
+
+osrf_connect($config);
+
+my $writer = OpenILS::Utils::EDIWriter->new({pretty => 1});
+#my $writer = OpenILS::Utils::EDIWriter->new;
+my $edi = $writer->write($po_id);
+
+print "$edi\n";
+
+

-----------------------------------------------------------------------

Summary of changes:
 Open-ILS/examples/fm_IDL.xml                       |   62 ++
 .../Validator/Acq/PurchaseOrderEDIRequired.pm      |   10 +-
 .../src/perlmods/lib/OpenILS/Utils/EDIWriter.pm    |  647 ++++++++++++++++++++
 Open-ILS/src/sql/Pg/002.schema.config.sql          |    2 +-
 Open-ILS/src/sql/Pg/200.schema.acq.sql             |   23 +-
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |  164 +++++
 .../sql/Pg/upgrade/1062.schema.edi_attr_set.sql    |   32 +
 .../1063.schema.inheritance-constraint-trigger.sql |   46 ++
 .../Pg/upgrade/1064.schema.issuance_scap_fkey.sql  |   20 +
 .../1065.schema.serial_pattern_templates.sql       |   25 +
 .../src/sql/Pg/upgrade/1066.data.spt_perms.sql     |   24 +
 .../src/sql/Pg/upgrade/1067.data.edi_attr_set.sql  |  171 ++++++
 .../XXXX.schema.inheritance-constraint-trigger.sql |   44 --
 .../XXXX.schema.serial_pattern_templates.sql       |   25 -
 .../src/sql/Pg/upgrade/YYYY.data.spt_perms.sql     |   24 -
 .../Pg/upgrade/ZZZZ.schema.issuance_scap_fkey.sql  |   18 -
 Open-ILS/src/support-scripts/edi_order_pusher.pl   |  234 +++++++
 .../src/support-scripts/test-scripts/edi_writer.pl |   25 +
 Open-ILS/src/templates/staff/admin/acq/index.tt2   |    8 +
 .../templates/staff/admin/acq/t_edi_attr_set.tt2   |   71 +++
 .../src/templates/staff/admin/acq/t_splash.tt2     |    1 +
 Open-ILS/src/templates/staff/css/style.css.tt2     |    7 +
 Open-ILS/web/js/ui/default/staff/admin/acq/app.js  |  211 +++++++
 Open-ILS/web/js/ui/default/staff/services/pcrud.js |   10 +-
 Open-ILS/web/js/ui/default/staff/services/ui.js    |    1 +
 .../Administration/acq-edi-attrs.adoc              |   50 ++
 26 files changed, 1834 insertions(+), 121 deletions(-)
 create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/Utils/EDIWriter.pm
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1062.schema.edi_attr_set.sql
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1063.schema.inheritance-constraint-trigger.sql
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1064.schema.issuance_scap_fkey.sql
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1065.schema.serial_pattern_templates.sql
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1066.data.spt_perms.sql
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1067.data.edi_attr_set.sql
 delete mode 100644 Open-ILS/src/sql/Pg/upgrade/XXXX.schema.inheritance-constraint-trigger.sql
 delete mode 100644 Open-ILS/src/sql/Pg/upgrade/XXXX.schema.serial_pattern_templates.sql
 delete mode 100644 Open-ILS/src/sql/Pg/upgrade/YYYY.data.spt_perms.sql
 delete mode 100644 Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.issuance_scap_fkey.sql
 create mode 100755 Open-ILS/src/support-scripts/edi_order_pusher.pl
 create mode 100755 Open-ILS/src/support-scripts/test-scripts/edi_writer.pl
 create mode 100644 Open-ILS/src/templates/staff/admin/acq/t_edi_attr_set.tt2
 create mode 100644 docs/RELEASE_NOTES_NEXT/Administration/acq-edi-attrs.adoc


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list