From 2de2799ae0606dbad4280c348f80ef08dd0bbe74 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Fri, 27 Jul 2012 16:11:54 -0400 Subject: [PATCH] Vandelay Copy Overlay support 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 Signed-off-by: Lebbeous Fogle-Weekley --- Open-ILS/examples/fm_IDL.xml | 2 + .../perlmods/lib/OpenILS/Application/Vandelay.pm | 257 +++++++++--- Open-ILS/src/sql/Pg/002.schema.config.sql | 2 +- Open-ILS/src/sql/Pg/012.schema.vandelay.sql | 4 +- Open-ILS/src/sql/Pg/950.data.seed-values.sql | 4 +- Open-ILS/src/sql/Pg/999.functions.global.sql | 15 +- .../upgrade/0731.schema.vandelay_item_overlay.sql | 452 +++++++++++++++++++++ Open-ILS/src/templates/vandelay/inc/upload.tt2 | 1 + Open-ILS/web/js/ui/default/vandelay/vandelay.js | 13 +- docs/RELEASE_NOTES_NEXT/vandelay-copy-overlay.txt | 37 ++ 10 files changed, 724 insertions(+), 63 deletions(-) create mode 100644 Open-ILS/src/sql/Pg/upgrade/0731.schema.vandelay_item_overlay.sql create mode 100644 docs/RELEASE_NOTES_NEXT/vandelay-copy-overlay.txt diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index f56bc08dbc..243f918963 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -210,6 +210,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + @@ -261,6 +262,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Vandelay.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Vandelay.pm index 89a7d8daab..5964361284 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Vandelay.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Vandelay.pm @@ -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; diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql index a6929690a1..9f2ba16bbb 100644 --- a/Open-ILS/src/sql/Pg/002.schema.config.sql +++ b/Open-ILS/src/sql/Pg/002.schema.config.sql @@ -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, diff --git a/Open-ILS/src/sql/Pg/012.schema.vandelay.sql b/Open-ILS/src/sql/Pg/012.schema.vandelay.sql index 65e2ca70cb..c2bf418645 100644 --- a/Open-ILS/src/sql/Pg/012.schema.vandelay.sql +++ b/Open-ILS/src/sql/Pg/012.schema.vandelay.sql @@ -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 ( 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 d4b88260f5..841d95cacc 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -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')) ; diff --git a/Open-ILS/src/sql/Pg/999.functions.global.sql b/Open-ILS/src/sql/Pg/999.functions.global.sql index c1df4afede..02f2861482 100644 --- a/Open-ILS/src/sql/Pg/999.functions.global.sql +++ b/Open-ILS/src/sql/Pg/999.functions.global.sql @@ -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 index 0000000000..145228b369 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/0731.schema.vandelay_item_overlay.sql @@ -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; diff --git a/Open-ILS/src/templates/vandelay/inc/upload.tt2 b/Open-ILS/src/templates/vandelay/inc/upload.tt2 index 25d42e6209..3e4237a2d9 100644 --- a/Open-ILS/src/templates/vandelay/inc/upload.tt2 +++ b/Open-ILS/src/templates/vandelay/inc/upload.tt2 @@ -9,6 +9,7 @@ jsId='vlUploadRecordType' onchange='vlShowUploadForm();'> + diff --git a/Open-ILS/web/js/ui/default/vandelay/vandelay.js b/Open-ILS/web/js/ui/default/vandelay/vandelay.js index 8e5f314d07..7d35503f3f 100644 --- a/Open-ILS/web/js/ui/default/vandelay/vandelay.js +++ b/Open-ILS/web/js/ui/default/vandelay/vandelay.js @@ -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 index 0000000000..70f0d15fe6 --- /dev/null +++ b/docs/RELEASE_NOTES_NEXT/vandelay-copy-overlay.txt @@ -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. + -- 2.11.0