Vandelay Copy Overlay support
authorBill Erickson <berick@esilibrary.com>
Fri, 27 Jul 2012 20:11:54 +0000 (16:11 -0400)
committerLebbeous Fogle-Weekley <lebbeous@esilibrary.com>
Tue, 31 Jul 2012 17:54:02 +0000 (13:54 -0400)
Vandelay Item Attributes (Cataloging -> MARC Import /
Export -> Import Item Attribute Definitions) contains
a new field called "Overlay Match ID".  The presence of data
in this field extracted from an import-item copy indicates
to the Vandelay import process that a copy overlay is requested
instead of new copy creation.  The value for the field is the
copy id for bib record queues and the ACQ lineitem_detail ID for
Acquisitions Queues.  For either type of queue, however, overlay
occurs against a real copy (asset.copy).  In the ACQ queue case,
we use the lineitem_detail ID because this is the data ACQ
providers and sub-systems will have access to.

When a match point ID value is a set and a matching copy is found,
the values extracted from the inbound copy data are used to replace
values on the existing found copy, including the call-number label.
Any fields on the inbound copy that are empty are ignored.

One use case for this feature are shelf-ready items produced by a
3rd-party (e.g. ACQ provider) and delivered to the library via MARC
file for upload.  The file might contain improved MARC bibliographic
data as well as real barcodes (i.e. not temporary ACQ generated
barcodes) for the copies already purchased through the vendor.

Permission
~~~~~~~~~

This adds a new permission called IMPORT_OVERLAY_COPY which is
required to perform the copy overlay step.

Regardless of permission, it is not possible to overlay values on
a copy unless the imported bib record links (creates/overlays/merges)
to/with the owning bib record for the copy to be overlaid.  This is
both for security and removal of a potent foot-gun.

Signed-off-by: Bill Erickson <berick@esilibrary.com>
Signed-off-by: Lebbeous Fogle-Weekley <lebbeous@esilibrary.com>

Open-ILS/examples/fm_IDL.xml
Open-ILS/src/perlmods/lib/OpenILS/Application/Vandelay.pm
Open-ILS/src/sql/Pg/002.schema.config.sql
Open-ILS/src/sql/Pg/012.schema.vandelay.sql
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/999.functions.global.sql
Open-ILS/src/sql/Pg/upgrade/0731.schema.vandelay_item_overlay.sql [new file with mode: 0644]
Open-ILS/src/templates/vandelay/inc/upload.tt2
Open-ILS/web/js/ui/default/vandelay/vandelay.js
docs/RELEASE_NOTES_NEXT/vandelay-copy-overlay.txt [new file with mode: 0644]

index f56bc08..243f918 100644 (file)
@@ -210,6 +210,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Public Note" name="pub_note" reporter:datatype="text"/>
                        <field reporter:label="Private Note" name="priv_note" reporter:datatype="text"/>
                        <field reporter:label="OPAC Visible" name="opac_visible" reporter:datatype="bool"/>
+                       <field reporter:label="Overlay Match ID" name="internal_id" reporter:datatype="int"/>
                </fields>
                <links>
                        <link field="import_error" reltype="has_a" key="code" map="" class="vie"/>
@@ -261,6 +262,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Private Note" name="priv_note" reporter:datatype="text"/>
                        <field reporter:label="OPAC Visible" name="opac_visible" reporter:datatype="text"/>
                        <field reporter:label="Copy Number" name="copy_number" reporter:datatype="text"/>
+                       <field reporter:label="Overlay Match ID" name="internal_id" reporter:datatype="text"/>
                </fields>
                <links>
                        <link field="owner" reltype="has_a" key="id" map="" class="aou"/>
index 89a7d8d..5964361 100644 (file)
@@ -1574,76 +1574,218 @@ sub import_record_asset_list_impl {
         for my $item_id (@$item_ids) {
             my $e = new_editor(requestor => $requestor, xact => 1);
             my $item = $e->retrieve_vandelay_import_item($item_id);
+            my ($copy, $vol, $evt);
+
             $$report_args{import_item} = $item;
             $$report_args{e} = $e;
             $$report_args{import_error} = undef;
             $$report_args{evt} = undef;
 
-            # --------------------------------------------------------------------------------
-            # Find or create the volume
-            # --------------------------------------------------------------------------------
-            my ($vol, $evt) =
-                OpenILS::Application::Cat::AssetCommon->find_or_create_volume(
-                    $e, $item->call_number, $rec->imported_as, $item->owning_lib);
+            if (my $copy_id = $item->internal_id) { # assignment
+                # copy matches an existing copy.  Overlay instead of create.
 
-            if($evt) {
+                $logger->info("vl: performing copy overlay for internal_id=$copy_id");
 
-                $$report_args{evt} = $evt;
-                respond_with_status($report_args);
-                next;
-            }
+                my $qt = $e->json_query({
+                    select => {vbq => ['queue_type']},
+                    from => {vqbr => 'vbq'},
+                    where => {'+vqbr' => {id => $rec_id}}
+                })->[0]->{queue_type};
 
-            # --------------------------------------------------------------------------------
-            # Create the new copy
-            # --------------------------------------------------------------------------------
-            my $copy = Fieldmapper::asset::copy->new;
-            $copy->loan_duration(2);
-            $copy->fine_level(2);
-            $copy->barcode($item->barcode);
-            $copy->location($item->location);
-            $copy->circ_lib($item->circ_lib || $item->owning_lib);
-            $copy->status( defined($item->status) ? $item->status : OILS_COPY_STATUS_IN_PROCESS );
-            $copy->circulate($item->circulate);
-            $copy->deposit($item->deposit);
-            $copy->deposit_amount($item->deposit_amount);
-            $copy->ref($item->ref);
-            $copy->holdable($item->holdable);
-            $copy->price($item->price);
-            $copy->circ_as_type($item->circ_as_type);
-            $copy->alert_message($item->alert_message);
-            $copy->opac_visible($item->opac_visible);
-            $copy->circ_modifier($item->circ_modifier);
+                if ($qt eq 'acq') {
+                    # internal_id for ACQ queues refers to acq.lineitem_detail.id
+                    # pull the real copy id from the acq LID
 
-            # --------------------------------------------------------------------------------
-            # Check for dupe barcode
-            # --------------------------------------------------------------------------------
-            if($evt = OpenILS::Application::Cat::AssetCommon->create_copy($e, $vol, $copy)) {
-                $$report_args{evt} = $evt;
-                $$report_args{import_error} = 'import.item.duplicate.barcode'
-                    if $evt->{textcode} eq 'ITEM_BARCODE_EXISTS';
-                respond_with_status($report_args);
-                next;
-            }
+                    my $lid = $e->retrieve_acq_lineitem_detail($copy_id);
+                    if (!$lid) {
+                        $$report_args{evt} = $e->die_event;
+                        respond_with_status($report_args);
+                        next;
+                    }
+                    $copy_id = $lid->eg_copy_id;
+                    $logger->info("vl: performing ACQ copy overlay for copy $copy_id");
+                }
 
-            # --------------------------------------------------------------------------------
-            # create copy notes
-            # --------------------------------------------------------------------------------
-            $evt = OpenILS::Application::Cat::AssetCommon->create_copy_note(
-                $e, $copy, '', $item->pub_note, 1) if $item->pub_note;
+                $copy = $e->search_asset_copy([
+                    {id => $copy_id, deleted => 'f'},
+                    {flesh => 1, flesh_fields => {acp => ['call_number']}}
+                ])->[0];
 
-            if($evt) {
-                $$report_args{evt} = $evt;
-                respond_with_status($report_args);
-                next;
-            }
+                if (!$copy) {
+                    $$report_args{evt} = $e->die_event;
+                    respond_with_status($report_args);
+                    next;
+                }
 
-            $evt = OpenILS::Application::Cat::AssetCommon->create_copy_note(
-                $e, $copy, '', $item->priv_note) if $item->priv_note;
+                # prevent update of unrelated copies
+                if ($copy->call_number->record != $rec->imported_as) {
+                    $logger->info("vl: attempt to overlay unrelated copy=$copy_id; rec=".$rec->imported_as);
 
-            if($evt) {
-                $$report_args{evt} = $evt;
-                respond_with_status($report_args);
-                next;
+                    $evt = OpenILS::Event->new('INVALID_IMPORT_COPY_ID', 
+                        note => 'Cannot overlay copies for unlinked bib',
+                        bre => $rec->imported_as, 
+                        copy_id => $copy_id
+                    );
+                    $$report_args{evt} = $evt;
+                    respond_with_status($report_args);
+                    next;
+                }
+
+                # overlaying copies requires an extra permission
+                if (!$e->allowed("IMPORT_OVERLAY_COPY", $copy->call_number->owning_lib)) {
+                    $$report_args{evt} = $e->die_event;
+                    respond_with_status($report_args);
+                    next;
+                }
+
+                # are we updating the call-number?
+                if ($item->call_number and $item->call_number ne $copy->call_number->label) {
+
+                    my $count = $e->json_query({
+                        select => {acp => [{
+                            alias => 'count', 
+                            column => 'id', 
+                            transform => 'count', 
+                            aggregate => 1
+                        }]},
+                        from => 'acp',
+                        where => {
+                            deleted => 'f',
+                            call_number => $copy->call_number->id
+                        }
+                    })->[0]->{count};
+
+                    if ($count == 1) {
+                        # if this is the only copy attached to this 
+                        # callnumber, just update the callnumber
+
+                        $logger->info("vl: updating callnumber label in copy overlay");
+
+                        $copy->call_number->label($item->call_number);
+                        if (!$e->update_asset_call_number($copy->call_number)) {
+                            $$report_args{evt} = $e->die_event;
+                            respond_with_status($report_args);
+                            next;
+                        }
+
+                    } else {
+
+                        # otherwise, move the copy to a new/existing 
+                        # call-number with the given label/owner
+                        # note that overlay does not allow the owning_lib 
+                        # to be changed.  Should it?
+
+                        $logger->info("vl: moving copy to new callnumber in copy overlay");
+
+                        ($vol, $evt) =
+                            OpenILS::Application::Cat::AssetCommon->find_or_create_volume(
+                                $e, $item->call_number, 
+                                $copy->call_number->record, 
+                                $copy->call_number->owning_lib
+                            );
+
+                        if($evt) {
+                            $$report_args{evt} = $evt;
+                            respond_with_status($report_args);
+                            next;
+                        }
+
+                        $copy->call_number($vol);
+                    }
+                } # cn-update
+
+                # for every field that has a non-'' value, overlay the copy value
+                foreach (qw/ barcode location circ_lib status 
+                    circulate deposit deposit_amount ref holdable 
+                    price circ_as_type alert_message opac_visible circ_modifier/) {
+
+                    my $val = $item->$_();
+                    $copy->$_($val) if $val and $val ne '';
+                }
+
+                # de-flesh for update
+                $copy->call_number($copy->call_number->id);
+                $copy->ischanged(1);
+
+                $evt = OpenILS::Application::Cat::AssetCommon->
+                    update_fleshed_copies($e, {all => 1}, undef, [$copy]);
+
+                if($evt) {
+                    $$report_args{evt} = $evt;
+                    respond_with_status($report_args);
+                    next;
+                }
+
+            } else { 
+
+                # Creating a new copy
+                $logger->info("vl: creating new copy in import");
+
+                # --------------------------------------------------------------------------------
+                # Find or create the volume
+                # --------------------------------------------------------------------------------
+                my ($vol, $evt) =
+                    OpenILS::Application::Cat::AssetCommon->find_or_create_volume(
+                        $e, $item->call_number, $rec->imported_as, $item->owning_lib);
+
+                if($evt) {
+                    $$report_args{evt} = $evt;
+                    respond_with_status($report_args);
+                    next;
+                }
+
+                # --------------------------------------------------------------------------------
+                # Create the new copy
+                # --------------------------------------------------------------------------------
+                $copy = Fieldmapper::asset::copy->new;
+                $copy->loan_duration(2);
+                $copy->fine_level(2);
+                $copy->barcode($item->barcode);
+                $copy->location($item->location);
+                $copy->circ_lib($item->circ_lib || $item->owning_lib);
+                $copy->status( defined($item->status) ? $item->status : OILS_COPY_STATUS_IN_PROCESS );
+                $copy->circulate($item->circulate);
+                $copy->deposit($item->deposit);
+                $copy->deposit_amount($item->deposit_amount);
+                $copy->ref($item->ref);
+                $copy->holdable($item->holdable);
+                $copy->price($item->price);
+                $copy->circ_as_type($item->circ_as_type);
+                $copy->alert_message($item->alert_message);
+                $copy->opac_visible($item->opac_visible);
+                $copy->circ_modifier($item->circ_modifier);
+
+                # --------------------------------------------------------------------------------
+                # Check for dupe barcode
+                # --------------------------------------------------------------------------------
+                if($evt = OpenILS::Application::Cat::AssetCommon->create_copy($e, $vol, $copy)) {
+                    $$report_args{evt} = $evt;
+                    $$report_args{import_error} = 'import.item.duplicate.barcode'
+                        if $evt->{textcode} eq 'ITEM_BARCODE_EXISTS';
+                    respond_with_status($report_args);
+                    next;
+                }
+
+                # --------------------------------------------------------------------------------
+                # create copy notes
+                # --------------------------------------------------------------------------------
+                $evt = OpenILS::Application::Cat::AssetCommon->create_copy_note(
+                    $e, $copy, '', $item->pub_note, 1) if $item->pub_note;
+
+                if($evt) {
+                    $$report_args{evt} = $evt;
+                    respond_with_status($report_args);
+                    next;
+                }
+
+                $evt = OpenILS::Application::Cat::AssetCommon->create_copy_note(
+                    $e, $copy, '', $item->priv_note) if $item->priv_note;
+
+                if($evt) {
+                    $$report_args{evt} = $evt;
+                    respond_with_status($report_args);
+                    next;
+                }
             }
 
             # set the import data on the import item
@@ -1664,7 +1806,6 @@ sub import_record_asset_list_impl {
             respond_with_status($report_args);
             $logger->info("vl: successfully imported item " . $item->barcode);
         }
-
     }
 
     $roe->rollback;
index a692969..9f2ba16 100644 (file)
@@ -87,7 +87,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 ('0730', :eg_version); -- miker/senator/berick
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0731', :eg_version); -- berick/senator
 
 CREATE TABLE config.bib_source (
        id              SERIAL  PRIMARY KEY,
index 65e2ca7..c2bf418 100644 (file)
@@ -104,6 +104,7 @@ CREATE TABLE vandelay.import_item_attr_definition (
     pub_note        TEXT,
     priv_note_title TEXT,
     priv_note       TEXT,
+    internal_id     TEXT,
        CONSTRAINT vand_import_item_attr_def_idx UNIQUE (owner,name)
 );
 
@@ -173,7 +174,8 @@ CREATE TABLE vandelay.import_item (
     alert_message   TEXT,
     pub_note        TEXT,
     priv_note       TEXT,
-    opac_visible    BOOL
+    opac_visible    BOOL,
+    internal_id     BIGINT -- queue_type == 'acq' ? acq.lineitem_detail.id : asset.copy.id
 );
  
 CREATE TABLE vandelay.import_bib_trash_fields (
index d4b8826..841d95c 100644 (file)
@@ -1559,7 +1559,9 @@ INSERT INTO permission.perm_list ( id, code, description ) VALUES
  ( 534, 'ADMIN_USER_ACTIVITY_TYPE', oils_i18n_gettext( 534,
     'Allows a user to create/retrieve/update/delete user activity types', 'ppl', 'description' )),
 ( 535, 'VIEW_TRIGGER_EVENT', oils_i18n_gettext( 535,
-    'Allows a user to view circ- and hold-related action/trigger events', 'ppl', 'description'))
+    'Allows a user to view circ- and hold-related action/trigger events', 'ppl', 'description')),
+( 536, 'IMPORT_OVERLAY_COPY', oils_i18n_gettext( 536,
+    'Allows a user to overlay copy data in MARC import', 'ppl', 'description'))
 ;
 
 
index c1df4af..02f2861 100644 (file)
@@ -1607,6 +1607,7 @@ DECLARE
     opac_visible    TEXT;
     pub_note        TEXT;
     priv_note       TEXT;
+    internal_id     TEXT;
 
     attr_def        RECORD;
     tmp_attr_set    RECORD;
@@ -1757,6 +1758,14 @@ BEGIN
                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.priv_note
             END;
 
+        internal_id :=
+            CASE
+                WHEN attr_def.internal_id IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.internal_id ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.internal_id || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.internal_id
+            END;
+
+
 
         xpath :=
             owning_lib      || '|' ||
@@ -1777,6 +1786,7 @@ BEGIN
             alert_message   || '|' ||
             pub_note        || '|' ||
             priv_note       || '|' ||
+            internal_id     || '|' ||
             opac_visible;
 
         FOR tmp_attr_set IN
@@ -1784,7 +1794,7 @@ BEGIN
                   FROM  oils_xpath_table( 'id', 'marc', 'vandelay.queued_bib_record', xpath, 'id = ' || import_id )
                             AS t( id INT, ol TEXT, clib TEXT, cn TEXT, cnum TEXT, cs TEXT, cl TEXT, circ TEXT,
                                   dep TEXT, dep_amount TEXT, r TEXT, hold TEXT, pr TEXT, bc TEXT, circ_mod TEXT,
-                                  circ_as TEXT, amessage TEXT, note TEXT, pnote TEXT, opac_vis TEXT )
+                                  circ_as TEXT, amessage TEXT, note TEXT, pnote TEXT, internal_id TEXT, opac_vis TEXT )
         LOOP
 
             attr_set.import_error := NULL;
@@ -1925,6 +1935,7 @@ BEGIN
             attr_set.pub_note       := tmp_attr_set.note; -- TEXT,
             attr_set.priv_note      := tmp_attr_set.pnote; -- TEXT,
             attr_set.alert_message  := tmp_attr_set.amessage; -- TEXT,
+            attr_set.internal_id    := tmp_attr_set.internal_id::BIGINT;
 
             RETURN NEXT attr_set;
 
@@ -1971,6 +1982,7 @@ BEGIN
             alert_message,
             pub_note,
             priv_note,
+            internal_id,
             opac_visible,
             import_error,
             error_detail
@@ -1995,6 +2007,7 @@ BEGIN
             item_data.alert_message,
             item_data.pub_note,
             item_data.priv_note,
+            item_data.internal_id,
             item_data.opac_visible,
             item_data.import_error,
             item_data.error_detail
diff --git a/Open-ILS/src/sql/Pg/upgrade/0731.schema.vandelay_item_overlay.sql b/Open-ILS/src/sql/Pg/upgrade/0731.schema.vandelay_item_overlay.sql
new file mode 100644 (file)
index 0000000..145228b
--- /dev/null
@@ -0,0 +1,452 @@
+-- 0731.schema.vandelay_item_overlay.sql
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('0731', :eg_version);
+
+ALTER TABLE vandelay.import_item_attr_definition 
+    ADD COLUMN internal_id TEXT; 
+
+ALTER TABLE vandelay.import_item 
+    ADD COLUMN internal_id BIGINT;
+
+INSERT INTO permission.perm_list ( id, code, description ) VALUES
+( 536, 'IMPORT_OVERLAY_COPY', oils_i18n_gettext( 536,
+    'Allows a user to overlay copy data in MARC import', 'ppl', 'description'));
+
+CREATE OR REPLACE FUNCTION vandelay.ingest_bib_items ( ) RETURNS TRIGGER AS $func$
+DECLARE
+    attr_def    BIGINT;
+    item_data   vandelay.import_item%ROWTYPE;
+BEGIN
+
+    IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
+        RETURN NEW;
+    END IF;
+
+    SELECT item_attr_def INTO attr_def FROM vandelay.bib_queue WHERE id = NEW.queue;
+
+    FOR item_data IN SELECT * FROM vandelay.ingest_items( NEW.id::BIGINT, attr_def ) LOOP
+        INSERT INTO vandelay.import_item (
+            record,
+            definition,
+            owning_lib,
+            circ_lib,
+            call_number,
+            copy_number,
+            status,
+            location,
+            circulate,
+            deposit,
+            deposit_amount,
+            ref,
+            holdable,
+            price,
+            barcode,
+            circ_modifier,
+            circ_as_type,
+            alert_message,
+            pub_note,
+            priv_note,
+            internal_id,
+            opac_visible,
+            import_error,
+            error_detail
+        ) VALUES (
+            NEW.id,
+            item_data.definition,
+            item_data.owning_lib,
+            item_data.circ_lib,
+            item_data.call_number,
+            item_data.copy_number,
+            item_data.status,
+            item_data.location,
+            item_data.circulate,
+            item_data.deposit,
+            item_data.deposit_amount,
+            item_data.ref,
+            item_data.holdable,
+            item_data.price,
+            item_data.barcode,
+            item_data.circ_modifier,
+            item_data.circ_as_type,
+            item_data.alert_message,
+            item_data.pub_note,
+            item_data.priv_note,
+            item_data.internal_id,
+            item_data.opac_visible,
+            item_data.import_error,
+            item_data.error_detail
+        );
+    END LOOP;
+
+    RETURN NULL;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+
+CREATE OR REPLACE FUNCTION vandelay.ingest_items ( import_id BIGINT, attr_def_id BIGINT ) RETURNS SETOF vandelay.import_item AS $$
+DECLARE
+
+    owning_lib      TEXT;
+    circ_lib        TEXT;
+    call_number     TEXT;
+    copy_number     TEXT;
+    status          TEXT;
+    location        TEXT;
+    circulate       TEXT;
+    deposit         TEXT;
+    deposit_amount  TEXT;
+    ref             TEXT;
+    holdable        TEXT;
+    price           TEXT;
+    barcode         TEXT;
+    circ_modifier   TEXT;
+    circ_as_type    TEXT;
+    alert_message   TEXT;
+    opac_visible    TEXT;
+    pub_note        TEXT;
+    priv_note       TEXT;
+    internal_id     TEXT;
+
+    attr_def        RECORD;
+    tmp_attr_set    RECORD;
+    attr_set        vandelay.import_item%ROWTYPE;
+
+    xpath           TEXT;
+    tmp_str         TEXT;
+
+BEGIN
+
+    SELECT * INTO attr_def FROM vandelay.import_item_attr_definition WHERE id = attr_def_id;
+
+    IF FOUND THEN
+
+        attr_set.definition := attr_def.id;
+
+        -- Build the combined XPath
+
+        owning_lib :=
+            CASE
+                WHEN attr_def.owning_lib IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.owning_lib ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.owning_lib || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.owning_lib
+            END;
+
+        circ_lib :=
+            CASE
+                WHEN attr_def.circ_lib IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.circ_lib ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circ_lib || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circ_lib
+            END;
+
+        call_number :=
+            CASE
+                WHEN attr_def.call_number IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.call_number ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.call_number || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.call_number
+            END;
+
+        copy_number :=
+            CASE
+                WHEN attr_def.copy_number IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.copy_number ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.copy_number || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.copy_number
+            END;
+
+        status :=
+            CASE
+                WHEN attr_def.status IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.status ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.status || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.status
+            END;
+
+        location :=
+            CASE
+                WHEN attr_def.location IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.location ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.location || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.location
+            END;
+
+        circulate :=
+            CASE
+                WHEN attr_def.circulate IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.circulate ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circulate || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circulate
+            END;
+
+        deposit :=
+            CASE
+                WHEN attr_def.deposit IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.deposit ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.deposit || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.deposit
+            END;
+
+        deposit_amount :=
+            CASE
+                WHEN attr_def.deposit_amount IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.deposit_amount ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.deposit_amount || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.deposit_amount
+            END;
+
+        ref :=
+            CASE
+                WHEN attr_def.ref IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.ref ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.ref || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.ref
+            END;
+
+        holdable :=
+            CASE
+                WHEN attr_def.holdable IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.holdable ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.holdable || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.holdable
+            END;
+
+        price :=
+            CASE
+                WHEN attr_def.price IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.price ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.price || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.price
+            END;
+
+        barcode :=
+            CASE
+                WHEN attr_def.barcode IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.barcode ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.barcode || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.barcode
+            END;
+
+        circ_modifier :=
+            CASE
+                WHEN attr_def.circ_modifier IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.circ_modifier ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circ_modifier || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circ_modifier
+            END;
+
+        circ_as_type :=
+            CASE
+                WHEN attr_def.circ_as_type IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.circ_as_type ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circ_as_type || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circ_as_type
+            END;
+
+        alert_message :=
+            CASE
+                WHEN attr_def.alert_message IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.alert_message ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.alert_message || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.alert_message
+            END;
+
+        opac_visible :=
+            CASE
+                WHEN attr_def.opac_visible IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.opac_visible ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.opac_visible || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.opac_visible
+            END;
+
+        pub_note :=
+            CASE
+                WHEN attr_def.pub_note IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.pub_note ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.pub_note || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.pub_note
+            END;
+        priv_note :=
+            CASE
+                WHEN attr_def.priv_note IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.priv_note ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.priv_note || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.priv_note
+            END;
+
+        internal_id :=
+            CASE
+                WHEN attr_def.internal_id IS NULL THEN 'null()'
+                WHEN LENGTH( attr_def.internal_id ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.internal_id || '"]'
+                ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.internal_id
+            END;
+
+
+
+        xpath :=
+            owning_lib      || '|' ||
+            circ_lib        || '|' ||
+            call_number     || '|' ||
+            copy_number     || '|' ||
+            status          || '|' ||
+            location        || '|' ||
+            circulate       || '|' ||
+            deposit         || '|' ||
+            deposit_amount  || '|' ||
+            ref             || '|' ||
+            holdable        || '|' ||
+            price           || '|' ||
+            barcode         || '|' ||
+            circ_modifier   || '|' ||
+            circ_as_type    || '|' ||
+            alert_message   || '|' ||
+            pub_note        || '|' ||
+            priv_note       || '|' ||
+            internal_id     || '|' ||
+            opac_visible;
+
+        FOR tmp_attr_set IN
+                SELECT  *
+                  FROM  oils_xpath_table( 'id', 'marc', 'vandelay.queued_bib_record', xpath, 'id = ' || import_id )
+                            AS t( id INT, ol TEXT, clib TEXT, cn TEXT, cnum TEXT, cs TEXT, cl TEXT, circ TEXT,
+                                  dep TEXT, dep_amount TEXT, r TEXT, hold TEXT, pr TEXT, bc TEXT, circ_mod TEXT,
+                                  circ_as TEXT, amessage TEXT, note TEXT, pnote TEXT, internal_id TEXT, opac_vis TEXT )
+        LOOP
+
+            attr_set.import_error := NULL;
+            attr_set.error_detail := NULL;
+            attr_set.deposit_amount := NULL;
+            attr_set.copy_number := NULL;
+            attr_set.price := NULL;
+
+            IF tmp_attr_set.pr != '' THEN
+                tmp_str = REGEXP_REPLACE(tmp_attr_set.pr, E'[^0-9\\.]', '', 'g');
+                IF tmp_str = '' THEN 
+                    attr_set.import_error := 'import.item.invalid.price';
+                    attr_set.error_detail := tmp_attr_set.pr; -- original value
+                    RETURN NEXT attr_set; CONTINUE; 
+                END IF;
+                attr_set.price := tmp_str::NUMERIC(8,2); 
+            END IF;
+
+            IF tmp_attr_set.dep_amount != '' THEN
+                tmp_str = REGEXP_REPLACE(tmp_attr_set.dep_amount, E'[^0-9\\.]', '', 'g');
+                IF tmp_str = '' THEN 
+                    attr_set.import_error := 'import.item.invalid.deposit_amount';
+                    attr_set.error_detail := tmp_attr_set.dep_amount; 
+                    RETURN NEXT attr_set; CONTINUE; 
+                END IF;
+                attr_set.deposit_amount := tmp_str::NUMERIC(8,2); 
+            END IF;
+
+            IF tmp_attr_set.cnum != '' THEN
+                tmp_str = REGEXP_REPLACE(tmp_attr_set.cnum, E'[^0-9]', '', 'g');
+                IF tmp_str = '' THEN 
+                    attr_set.import_error := 'import.item.invalid.copy_number';
+                    attr_set.error_detail := tmp_attr_set.cnum; 
+                    RETURN NEXT attr_set; CONTINUE; 
+                END IF;
+                attr_set.copy_number := tmp_str::INT; 
+            END IF;
+
+            IF tmp_attr_set.ol != '' THEN
+                SELECT id INTO attr_set.owning_lib FROM actor.org_unit WHERE shortname = UPPER(tmp_attr_set.ol); -- INT
+                IF NOT FOUND THEN
+                    attr_set.import_error := 'import.item.invalid.owning_lib';
+                    attr_set.error_detail := tmp_attr_set.ol;
+                    RETURN NEXT attr_set; CONTINUE; 
+                END IF;
+            END IF;
+
+            IF tmp_attr_set.clib != '' THEN
+                SELECT id INTO attr_set.circ_lib FROM actor.org_unit WHERE shortname = UPPER(tmp_attr_set.clib); -- INT
+                IF NOT FOUND THEN
+                    attr_set.import_error := 'import.item.invalid.circ_lib';
+                    attr_set.error_detail := tmp_attr_set.clib;
+                    RETURN NEXT attr_set; CONTINUE; 
+                END IF;
+            END IF;
+
+            IF tmp_attr_set.cs != '' THEN
+                SELECT id INTO attr_set.status FROM config.copy_status WHERE LOWER(name) = LOWER(tmp_attr_set.cs); -- INT
+                IF NOT FOUND THEN
+                    attr_set.import_error := 'import.item.invalid.status';
+                    attr_set.error_detail := tmp_attr_set.cs;
+                    RETURN NEXT attr_set; CONTINUE; 
+                END IF;
+            END IF;
+
+            IF tmp_attr_set.circ_mod != '' THEN
+                SELECT code INTO attr_set.circ_modifier FROM config.circ_modifier WHERE code = tmp_attr_set.circ_mod;
+                IF NOT FOUND THEN
+                    attr_set.import_error := 'import.item.invalid.circ_modifier';
+                    attr_set.error_detail := tmp_attr_set.circ_mod;
+                    RETURN NEXT attr_set; CONTINUE; 
+                END IF;
+            END IF;
+
+            IF tmp_attr_set.circ_as != '' THEN
+                SELECT code INTO attr_set.circ_as_type FROM config.coded_value_map WHERE ctype = 'item_type' AND code = tmp_attr_set.circ_as;
+                IF NOT FOUND THEN
+                    attr_set.import_error := 'import.item.invalid.circ_as_type';
+                    attr_set.error_detail := tmp_attr_set.circ_as;
+                    RETURN NEXT attr_set; CONTINUE; 
+                END IF;
+            END IF;
+
+            IF tmp_attr_set.cl != '' THEN
+
+                -- search up the org unit tree for a matching copy location
+                WITH RECURSIVE anscestor_depth AS (
+                    SELECT  ou.id,
+                        out.depth AS depth,
+                        ou.parent_ou
+                    FROM  actor.org_unit ou
+                        JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
+                    WHERE ou.id = COALESCE(attr_set.owning_lib, attr_set.circ_lib)
+                        UNION ALL
+                    SELECT  ou.id,
+                        out.depth,
+                        ou.parent_ou
+                    FROM  actor.org_unit ou
+                        JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
+                        JOIN anscestor_depth ot ON (ot.parent_ou = ou.id)
+                ) SELECT  cpl.id INTO attr_set.location
+                    FROM  anscestor_depth a
+                        JOIN asset.copy_location cpl ON (cpl.owning_lib = a.id)
+                    WHERE LOWER(cpl.name) = LOWER(tmp_attr_set.cl)
+                    ORDER BY a.depth DESC
+                    LIMIT 1; 
+
+                IF NOT FOUND THEN
+                    attr_set.import_error := 'import.item.invalid.location';
+                    attr_set.error_detail := tmp_attr_set.cs;
+                    RETURN NEXT attr_set; CONTINUE; 
+                END IF;
+            END IF;
+
+            attr_set.circulate      :=
+                LOWER( SUBSTRING( tmp_attr_set.circ, 1, 1)) IN ('t','y','1')
+                OR LOWER(tmp_attr_set.circ) = 'circulating'; -- BOOL
+
+            attr_set.deposit        :=
+                LOWER( SUBSTRING( tmp_attr_set.dep, 1, 1 ) ) IN ('t','y','1')
+                OR LOWER(tmp_attr_set.dep) = 'deposit'; -- BOOL
+
+            attr_set.holdable       :=
+                LOWER( SUBSTRING( tmp_attr_set.hold, 1, 1 ) ) IN ('t','y','1')
+                OR LOWER(tmp_attr_set.hold) = 'holdable'; -- BOOL
+
+            attr_set.opac_visible   :=
+                LOWER( SUBSTRING( tmp_attr_set.opac_vis, 1, 1 ) ) IN ('t','y','1')
+                OR LOWER(tmp_attr_set.opac_vis) = 'visible'; -- BOOL
+
+            attr_set.ref            :=
+                LOWER( SUBSTRING( tmp_attr_set.r, 1, 1 ) ) IN ('t','y','1')
+                OR LOWER(tmp_attr_set.r) = 'reference'; -- BOOL
+
+            attr_set.call_number    := tmp_attr_set.cn; -- TEXT
+            attr_set.barcode        := tmp_attr_set.bc; -- TEXT,
+            attr_set.alert_message  := tmp_attr_set.amessage; -- TEXT,
+            attr_set.pub_note       := tmp_attr_set.note; -- TEXT,
+            attr_set.priv_note      := tmp_attr_set.pnote; -- TEXT,
+            attr_set.alert_message  := tmp_attr_set.amessage; -- TEXT,
+            attr_set.internal_id    := tmp_attr_set.internal_id::BIGINT;
+
+            RETURN NEXT attr_set;
+
+        END LOOP;
+
+    END IF;
+
+    RETURN;
+
+END;
+$$ LANGUAGE PLPGSQL;
+
+
+
+COMMIT;
index 25d42e6..3e4237a 100644 (file)
@@ -9,6 +9,7 @@
                         jsId='vlUploadRecordType' onchange='vlShowUploadForm();'>
                     <option value='bib' selected='selected'>[% l('Bibliographic Records') %]</option>
                     <option value='auth'>[% l('Authority Records') %]</option>
+                    <option value='bib-acq'>[% l('Acquisitions Records') %]</option>
                 </select>
             </td>
         </tr>
index 8e5f314..7d35503 100644 (file)
@@ -375,10 +375,18 @@ function uploadMARC(onload){
 function createQueue(queueName, type, onload, importDefId, matchSet) {
     var name = (type=='bib') ? 'bib' : 'authority';
     var method = 'open-ils.vandelay.'+ name +'_queue.create'
+
+    var qType = name;
+    if (vlUploadRecordType.getValue().match(/acq/)) 
+        var qType = 'acq';
+
+    console.log('record type ' + vlUploadRecordType.getValue());
+    console.log('record type ' + vlUploadRecordType.getValue());
+
     fieldmapper.standardRequest(
         ['open-ils.vandelay', method],
         {   async: true,
-            params: [authtoken, queueName, null, name, matchSet, importDefId],
+            params: [authtoken, queueName, null, qType, matchSet, importDefId],
             oncomplete : function(r) {
                 var queue = r.recv().content();
                 if(e = openils.Event.parse(queue)) 
@@ -1313,6 +1321,9 @@ function batchUpload() {
     var queueName = dijit.byId('vl-queue-name').getValue();
     currentType = dijit.byId('vl-record-type').getValue();
 
+    // could be bib-acq, which makes no sense in most places
+    if (currentType.match(/bib/)) currentType = 'bib';
+
     var handleProcessSpool = function() {
         if( 
             vlUploadQueueImportNoMatch.checked || 
diff --git a/docs/RELEASE_NOTES_NEXT/vandelay-copy-overlay.txt b/docs/RELEASE_NOTES_NEXT/vandelay-copy-overlay.txt
new file mode 100644 (file)
index 0000000..70f0d15
--- /dev/null
@@ -0,0 +1,37 @@
+Vandelay (MARC Import/Export) Copy Overlay
+------------------------------------------
+
+Vandelay Item Attributes (Cataloging -> MARC Import / 
+Export -> Import Item Attribute Definitions) contains 
+a new field called "Overlay Match ID".  The presence of data 
+in this field extracted from an import-item copy indicates 
+to the Vandelay import process that a copy overlay is requested 
+instead of new copy creation.  The value for the field is the 
+copy id for bib record queues and the ACQ lineitem_detail ID for 
+Acquisitions Queues.  For either type of queue, however, overlay 
+occurs against a real copy (asset.copy).  In the ACQ queue case, 
+we use the lineitem_detail ID because this is the data ACQ 
+providers and sub-systems will have access to.
+
+When a match point ID value is a set and a matching copy is found, 
+the values extracted from the inbound copy data are used to replace 
+values on the existing found copy, including the call-number label.  
+Any fields on the inbound copy that are empty are ignored.
+
+One use case for this feature are shelf-ready items produced by a 
+3rd-party (e.g. ACQ provider) and delivered to the library via MARC
+file for upload.  The file might contain improved MARC bibliographic
+data as well as real barcodes (i.e. not temporary ACQ generated 
+barcodes) for the copies already purchased through the vendor.
+
+Permission
+~~~~~~~~~~
+
+This adds a new permission called IMPORT_OVERLAY_COPY which is 
+required to perform the copy overlay step.
+
+Regardless of permission, it is not possible to overlay values on
+a copy unless the imported bib record links (creates/overlays/merges)
+to/with the owning bib record for the copy to be overlaid.  This is 
+both for security and removal of a potent foot-gun.
+