Add support for Multi-Homed Items (aka Foreign Bibs, aka Linked Items)
authormiker <miker@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Tue, 12 Apr 2011 19:51:54 +0000 (19:51 +0000)
committermiker <miker@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Tue, 12 Apr 2011 19:51:54 +0000 (19:51 +0000)
Evergreen needs to support the ability to attach a barcoded item to more than one bibliographic record.  Use cases include:
  1. Barcoded E-Readers with preloaded content
    * Readers would all be items attached to a single "master" bib record in the traditional way, through call numbers that define their ownership
    * Each reader, as a barcoded item, can be attached through Multi-homed Items to records describing the list of preloaded content
    * These attached Multi-homed Items can be added and removed as content is swapped out on each reader
  2. Dual-language items
    * Cataloger decides which of several alternate languages is the primary, and attaches the barcoded item to that record in the traditional way
    * Alternate language records are attached to this item through Multi-homed Items
  3. "Back-to-back" books -- two books printed upside down relative to one another, with two "front" covers
    * Cataloger decides which of the two titles is the primary, and attaches the barcoded item to that record in the traditional way
    * Alternate title record is attached to this item through Multi-homed Items
  4. Bound Volumes -- Sets of individual works collected into a single barcoded package
    * Cataloger decides which of the titles is the primary (or creates a record for the collection as a whole), and attaches the barcoded item to that record in the traditional way
    * Remaining title records for the collected peices are attached to this item through Multi-homed Items

Functionality funded by Natural Resources Canada -- http://www.nrcan-rncan.gc.ca/com/

Please see http://git.esilibrary.com/?p=evergreen-equinox.git;a=shortlog;h=refs/heads/multi_home for the full commit history.

git-svn-id: svn://svn.open-ils.org/ILS/trunk@20056 dcc99617-32d9-48b4-a31d-7c20da2025e4

37 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/biblio.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/dbi.pm
Open-ILS/src/sql/Pg/002.schema.config.sql
Open-ILS/src/sql/Pg/010.schema.biblio.sql
Open-ILS/src/sql/Pg/030.schema.metabib.sql
Open-ILS/src/sql/Pg/040.schema.asset.sql
Open-ILS/src/sql/Pg/300.schema.staged_search.sql
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/990.schema.unapi.sql
Open-ILS/src/sql/Pg/999.functions.global.sql
Open-ILS/src/sql/Pg/upgrade/0512.schema.multi-home-items.sql [new file with mode: 0644]
Open-ILS/web/opac/common/js/config.js
Open-ILS/web/opac/locale/en-US/lang.dtd
Open-ILS/web/opac/locale/en-US/opac.dtd
Open-ILS/web/opac/skin/default/js/advanced.js
Open-ILS/web/opac/skin/default/js/copy_details.js
Open-ILS/web/opac/skin/default/js/rdetail.js
Open-ILS/web/opac/skin/default/xml/rdetail/rdetail_cn_details.xml
Open-ILS/web/opac/skin/default/xml/rdetail/rdetail_extras.xml
Open-ILS/xul/staff_client/chrome/content/OpenILS/data.js
Open-ILS/xul/staff_client/chrome/content/OpenILS/global_util.js
Open-ILS/xul/staff_client/chrome/content/cat/opac.js
Open-ILS/xul/staff_client/chrome/content/cat/opac.xul
Open-ILS/xul/staff_client/chrome/content/main/constants.js
Open-ILS/xul/staff_client/chrome/content/main/menu.js
Open-ILS/xul/staff_client/chrome/content/util/exec.js
Open-ILS/xul/staff_client/chrome/content/util/widget_prompt.js [new file with mode: 0644]
Open-ILS/xul/staff_client/chrome/content/util/widget_prompt.xul [new file with mode: 0644]
Open-ILS/xul/staff_client/chrome/locale/en-US/offline.properties
Open-ILS/xul/staff_client/server/cat/copy_browser.js
Open-ILS/xul/staff_client/server/cat/copy_browser.xul
Open-ILS/xul/staff_client/server/cat/manage_multi_home_items.js [new file with mode: 0644]
Open-ILS/xul/staff_client/server/cat/manage_multi_home_items.xul [new file with mode: 0644]
Open-ILS/xul/staff_client/server/locale/en-US/cat.properties

index 04b8504..11faf84 100644 (file)
@@ -84,6 +84,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field name="copy_count" oils_persist:virtual="true" />
                        <field name="series" oils_persist:virtual="true" />
                        <field name="serials" oils_persist:virtual="true" />
+                       <field name="foreign_copy_maps" oils_persist:virtual="true" />
                </fields>
        </class>
 
@@ -1045,6 +1046,50 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
         </permacrud>
        </class>
 
+       <class id="bpt" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="biblio::peer_type" oils_persist:tablename="biblio.peer_type" reporter:label="Bibliographic Record Peer Type" oils_persist:field_safe="true">
+               <fields oils_persist:primary="id" oils_persist:sequence="biblio.peer_type_id_seq">
+                       <field reporter:label="ID" name="id" reporter:selector="name" reporter:datatype="id"/>
+                       <field reporter:label="Name" name="name" reporter:datatype="text" oils_persist:i18n="true"/>
+               </fields>
+               <links/>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="CREATE_BIB_PTYPE" global_required="true"/>
+                <retrieve/>
+                <update permission="UPDATE_BIB_PTYPE" global_required="true"/>
+                <delete permission="DELETE_BIB_PTYPE" global_required="true"/>
+            </actions>
+        </permacrud>
+       </class>
+
+       <class id="bpbcm" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="biblio::peer_bib_copy_map" oils_persist:tablename="biblio.peer_bib_copy_map" reporter:label="Bibliographic Record Peer Copy Map">
+               <fields oils_persist:primary="id" oils_persist:sequence="biblio.peer_bib_copy_map_id_seq">
+                       <field reporter:label="ID" name="id" reporter:selector="name" reporter:datatype="id"/>
+                       <field reporter:label="Peer Type" name="peer_type" reporter:datatype="link"/>
+                       <field reporter:label="Peer Record" name="peer_record" reporter:datatype="link"/>
+                       <field reporter:label="Target Copy" name="target_copy" reporter:datatype="link"/>
+               </fields>
+        <links>
+            <link field="peer_type" reltype="has_a" key="id" map="" class="bpt"/>
+            <link field="peer_record" reltype="has_a" key="id" map="" class="bre"/>
+            <link field="target_copy" reltype="has_a" key="id" map="" class="acp"/>
+        </links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="UPDATE_COPY">
+                    <context link="target_copy" field="circ_lib"/>
+                </create>
+                <retrieve/>
+                <update permission="UPDATE_COPY">
+                    <context link="target_copy" field="circ_lib"/>
+                </update>
+                <delete permission="UPDATE_COPY">
+                    <context link="target_copy" field="circ_lib"/>
+                </delete>
+            </actions>
+        </permacrud>
+       </class>
+
        <class id="cbrebt" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="container::biblio_record_entry_bucket_type" oils_persist:tablename="container.biblio_record_entry_bucket_type" reporter:label="Bibliographic Record Bucket Type" oils_persist:field_safe="true">
                <fields oils_persist:primary="code">
                        <field reporter:label="Code" name="code" reporter:selector="name" reporter:datatype="id"/>
@@ -4770,6 +4815,8 @@ SELECT  usr,
                        <field reporter:label="Holds" name="holds" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="Statistical Category Entries" name="stat_cat_entries" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="Monograph Parts" name="parts" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Peer Record Maps" name="peer_record_maps" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Peer Records" name="peer_records" oils_persist:virtual="true" reporter:datatype="link"/>
                </fields>
                <links>
                        <link field="age_protect" reltype="has_a" key="id" map="" class="crahp"/>
@@ -4787,6 +4834,8 @@ SELECT  usr,
                        <link field="total_circ_count" reltype="might_have" key="id" map="" class="erfcc"/>
                        <link field="circ_modifier" reltype="has_a" key="code" map="" class="ccm"/>
                        <link field="parts" reltype="has_many" key="target_copy" map="part" class="acpm"/>
+                       <link field="peer_record_maps" reltype="has_many" key="target_copy" map="" class="bpbcm"/>
+                       <link field="peer_records" reltype="has_many" key="target_copy" map="peer_record" class="bpbcm"/>
                </links>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
             <actions>
index 88af4f5..cd3294e 100644 (file)
@@ -973,7 +973,7 @@ sub copy_details {
                        {
                                flesh => 2,
                                flesh_fields => {
-                                       acp => ['call_number','parts'],
+                                       acp => ['call_number','parts','peer_record_maps'],
                                        acn => ['record','prefix','suffix','label_class']
                                }
                        }
index 631a9be..5cd7087 100644 (file)
@@ -494,7 +494,7 @@ sub fleshed_copy_retrieve2 {
                 flesh        => 2,
                 flesh_fields => {
                     acp => [
-                        qw/ location status stat_cat_entry_copy_maps notes age_protect parts /
+                        qw/ location status stat_cat_entry_copy_maps notes age_protect parts peer_record_maps /
                     ],
                     ascecm => [qw/ stat_cat stat_cat_entry /],
                 }
@@ -568,16 +568,32 @@ __PACKAGE__->register_method(
     api_name      => 'open-ils.search.bib_id.by_barcode',
     authoritative => 1,
     signature => { 
-        desc   => 'Retrieve copy object with fleshed record, given the barcode',
+        desc   => 'Retrieve bib record id associated with the copy identified by the given barcode',
         params => [
             { desc => 'Item barcode', type => 'string' }
         ],
         return => {
-            desc => 'Asset copy object with fleshed record and callnumber, or event on error or null set'
+            desc => 'Bib record id.'
         }
     }
 );
 
+__PACKAGE__->register_method(
+    method        => 'title_id_by_item_barcode',
+    api_name      => 'open-ils.search.multi_home.bib_ids.by_barcode',
+    authoritative => 1,
+    signature => {
+        desc   => 'Retrieve bib record ids associated with the copy identified by the given barcode.  This includes peer bibs for Multi-Home items.',
+        params => [
+            { desc => 'Item barcode', type => 'string' }
+        ],
+        return => {
+            desc => 'Array of bib record ids.  First element is the native bib for the item.'
+        }
+    }
+);
+
+
 sub title_id_by_item_barcode {
     my( $self, $conn, $barcode ) = @_;
     my $e = new_editor();
@@ -595,9 +611,97 @@ sub title_id_by_item_barcode {
     );
 
     return $e->event unless @$copies;
-    return $$copies[0]->call_number->record->id;
+
+    if( $self->api_name =~ /multi_home/ ) {
+        my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
+            [
+                { target_copy => $$copies[0]->id }
+            ]
+        );
+        my @temp =  map { $_->peer_record } @{ $multi_home_list };
+        unshift @temp, $$copies[0]->call_number->record->id;
+        return \@temp;
+    } else {
+        return $$copies[0]->call_number->record->id;
+    }
 }
 
+__PACKAGE__->register_method(
+    method        => 'find_peer_bibs',
+    api_name      => 'open-ils.search.peer_bibs.test',
+    authoritative => 1,
+    signature => {
+        desc   => 'Tests to see if the specified record is a peer record.',
+        params => [
+            { desc => 'Biblio record entry Id', type => 'number' }
+        ],
+        return => {
+            desc => 'True if specified id can be found in biblio.peer_record_copy_map.peer_record.',
+            type => 'bool'
+        }
+    }
+);
+
+__PACKAGE__->register_method(
+    method        => 'find_peer_bibs',
+    api_name      => 'open-ils.search.peer_bibs',
+    authoritative => 1,
+    signature => {
+        desc   => 'Return acps and mvrs for multi-home items linked to specified peer record.',
+        params => [
+            { desc => 'Biblio record entry Id', type => 'number' }
+        ],
+        return => {
+            desc => '{ records => Array of mvrs, items => array of acps }',
+        }
+    }
+);
+
+
+sub find_peer_bibs {
+       my( $self, $client, $doc_id ) = @_;
+    my $e = new_editor();
+
+    my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
+        [
+            { peer_record => $doc_id },
+            {
+                flesh => 2,
+                flesh_fields => {
+                    bpbcm => [ 'target_copy', 'peer_type' ],
+                    acp => [ 'call_number', 'location', 'status', 'peer_record_maps' ]
+                }
+            }
+        ]
+    );
+
+    if ($self->api_name =~ /test/) {
+        return scalar( @{$multi_home_list} ) > 0 ? 1 : 0;
+    }
+
+    if (scalar(@{$multi_home_list})==0) {
+        return [];
+    }
+
+    # create a unique hash of the primary record MVRs for foreign copies
+    # XXX PLEASE let's change to unAPI2 (supports foreign copies) in the TT opac?!?
+    my %rec_hash = map {
+        ($_->target_copy->call_number->record, _records_to_mods( $_->target_copy->call_number->record )->[0])
+    } @$multi_home_list;
+
+    # set the foreign_copy_maps field to an empty array
+    map { $rec_hash{$_}->foreign_copy_maps([]) } keys( %rec_hash );
+
+    # push the maps onto the correct MVRs
+    for (@$multi_home_list) {
+        push(
+            @{$rec_hash{ $_->target_copy->call_number->record }->foreign_copy_maps()},
+            $_
+        );
+    }
+
+    return [sort {$a->title cmp $b->title} values(%rec_hash)];
+};
 
 __PACKAGE__->register_method(
     method   => "biblio_copy_to_mods",
index 3df2ee5..7780cbc 100644 (file)
@@ -23,6 +23,22 @@ biblio::record_note->columns( Essential => qw/id record value creator
 #-------------------------------------------------------------------------------
 
 #-------------------------------------------------------------------------------
+package biblio::peer_type;
+use base qw/biblio/;
+
+biblio::peer_type->table( 'biblio_peer_type' );
+biblio::peer_type->columns( Essential => qw/id name/ );
+#-------------------------------------------------------------------------------
+
+#-------------------------------------------------------------------------------
+package biblio::peer_record_copy_map;
+use base qw/biblio/;
+
+biblio::peer_record_copy_map->table( 'biblio_peer_record_copy_map' );
+biblio::peer_record_copy_map->columns( Essential => qw/id peer_type peer_record target_copy/ );
+#-------------------------------------------------------------------------------
+
+#-------------------------------------------------------------------------------
 package biblio::monograph_part;
 use base qw/biblio/;
 
index 623be5f..5c6990f 100644 (file)
     biblio::monograph_part->sequence( 'biblio.monograph_part_id_seq' );
 
        #-------------------------------------------------------------------------------
+       package biblio::peer_record_copy_map;
+
+       biblio::peer_record_copy_map->table( 'biblio.peer_record_copy_map' );
+       biblio::peer_record_copy_map->sequence( 'biblio.peer_record_copy_map_id_seq' );
+
+       #-------------------------------------------------------------------------------
+       package biblio::peer_type;
+
+       biblio::peer_type->table( 'biblio.peer_type' );
+       biblio::peer_type->sequence( 'biblio.peer_type_id_seq' );
+
+       #-------------------------------------------------------------------------------
        package container::user_bucket;
 
        container::user_bucket->table( 'container.user_bucket' );
index 43390da..55bff9f 100644 (file)
@@ -70,7 +70,7 @@ CREATE TABLE config.upgrade_log (
     install_date    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
 );
 
-INSERT INTO config.upgrade_log (version) VALUES ('0511'); -- miker
+INSERT INTO config.upgrade_log (version) VALUES ('0512'); -- miker
 
 CREATE TABLE config.bib_source (
        id              SERIAL  PRIMARY KEY,
index 036b7b6..4ce54ae 100644 (file)
@@ -79,6 +79,20 @@ CREATE INDEX biblio_record_note_record_idx ON biblio.record_note ( record );
 CREATE INDEX biblio_record_note_creator_idx ON biblio.record_note ( creator );
 CREATE INDEX biblio_record_note_editor_idx ON biblio.record_note ( editor );
 
+CREATE TABLE biblio.peer_type (
+    id      SERIAL  PRIMARY KEY,
+    name        TEXT        NOT NULL UNIQUE -- i18n
+);
+
+CREATE TABLE biblio.peer_bib_copy_map (
+    id      SERIAL  PRIMARY KEY,
+    peer_type   INT     NOT NULL REFERENCES biblio.peer_type (id),
+    peer_record BIGINT      NOT NULL REFERENCES biblio.record_entry (id),
+    target_copy BIGINT      NOT NULL -- can't use fkey because of acp subtables
+);
+CREATE INDEX peer_bib_copy_map_record_idx ON biblio.peer_bib_copy_map (peer_record);
+CREATE INDEX peer_bib_copy_map_copy_idx ON biblio.peer_bib_copy_map (target_copy);
+
 CREATE TABLE biblio.monograph_part (
     id              SERIAL  PRIMARY KEY,
     record          BIGINT  NOT NULL REFERENCES biblio.record_entry (id),
index 8530aaf..ad41652 100644 (file)
@@ -950,6 +950,7 @@ BEGIN
         DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id; -- Rid ourselves of the search-estimate-killing linkage
         DELETE FROM metabib.record_attr WHERE id = NEW.id; -- Kill the attrs hash, useless on deleted records
         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
+        DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = NEW.id; -- Separate any multi-homed items
         RETURN NEW; -- and we're done
     END IF;
 
index 73a55c4..6d1e41f 100644 (file)
@@ -99,7 +99,8 @@ CREATE TABLE asset.copy_part_map (
 CREATE UNIQUE INDEX copy_part_map_cp_part_idx ON asset.copy_part_map (target_copy, part);
 
 CREATE TABLE asset.opac_visible_copies (
-  id        BIGINT primary key, -- copy id
+  id        BIGSERIAL primary key,
+  copy_id   BIGINT, -- copy id
   record    BIGINT,
   circ_lib  INTEGER
 );
@@ -109,6 +110,8 @@ search.query_parser_fts() to speed up OPAC visibility checks on large
 databases.  Contents are maintained by a set of triggers.
 $$;
 CREATE INDEX opac_visible_copies_idx1 on asset.opac_visible_copies (record, circ_lib);
+CREATE INDEX opac_visible_copies_copy_id_idx on asset.opac_visible_copies (copy_id);
+CREATE UNIQUE INDEX opac_visible_copies_once_per_record_idx on asset.opac_visible_copies (copy_id, record);
 
 CREATE OR REPLACE FUNCTION asset.acp_status_changed()
 RETURNS TRIGGER AS $$
index 2ab2efd..e13bb60 100644 (file)
@@ -190,9 +190,20 @@ BEGIN
               LIMIT 1;
 
             IF NOT FOUND THEN
-                -- RAISE NOTICE ' % were all status-excluded ... ', core_result.records;
-                excluded_count := excluded_count + 1;
-                CONTINUE;
+                PERFORM 1
+                  FROM  biblio.peer_bib_copy_map pr
+                        JOIN asset.copy cp ON (cp.id = pr.target_copy)
+                  WHERE NOT cp.deleted
+                        AND cp.status IN ( SELECT * FROM search.explode_array( param_statuses ) )
+                        AND pr.peer_record IN ( SELECT * FROM search.explode_array( core_result.records ) )
+                        AND cp.circ_lib IN ( SELECT * FROM search.explode_array( search_org_list ) )
+                  LIMIT 1;
+
+                IF NOT FOUND THEN
+                -- RAISE NOTICE ' % and multi-home linked records were all status-excluded ... ', core_result.records;
+                    excluded_count := excluded_count + 1;
+                    CONTINUE;
+                END IF;
             END IF;
 
         END IF;
@@ -210,9 +221,20 @@ BEGIN
               LIMIT 1;
 
             IF NOT FOUND THEN
-                -- RAISE NOTICE ' % were all copy_location-excluded ... ', core_result.records;
-                excluded_count := excluded_count + 1;
-                CONTINUE;
+                PERFORM 1
+                  FROM  biblio.peer_bib_copy_map pr
+                        JOIN asset.copy cp ON (cp.id = pr.target_copy)
+                  WHERE NOT cp.deleted
+                        AND cp.location IN ( SELECT * FROM search.explode_array( param_locations ) )
+                        AND pr.peer_record IN ( SELECT * FROM search.explode_array( core_result.records ) )
+                        AND cp.circ_lib IN ( SELECT * FROM search.explode_array( search_org_list ) )
+                  LIMIT 1;
+
+                IF NOT FOUND THEN
+                    -- RAISE NOTICE ' % and multi-home linked records were all copy_location-excluded ... ', core_result.records;
+                    excluded_count := excluded_count + 1;
+                    CONTINUE;
+                END IF;
             END IF;
 
         END IF;
@@ -226,9 +248,19 @@ BEGIN
               LIMIT 1;
 
             IF NOT FOUND THEN
-                -- RAISE NOTICE ' % were all visibility-excluded ... ', core_result.records;
-                excluded_count := excluded_count + 1;
-                CONTINUE;
+                PERFORM 1
+                  FROM  biblio.peer_bib_copy_map pr
+                        JOIN asset.opac_visible_copies cp ON (cp.copy_id = pr.target_copy)
+                  WHERE cp.circ_lib IN ( SELECT * FROM search.explode_array( search_org_list ) )
+                        AND pr.peer_record IN ( SELECT * FROM search.explode_array( core_result.records ) )
+                  LIMIT 1;
+
+                IF NOT FOUND THEN
+
+                    -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
+                    excluded_count := excluded_count + 1;
+                    CONTINUE;
+                END IF;
             END IF;
 
         ELSE
@@ -236,7 +268,6 @@ BEGIN
             PERFORM 1
               FROM  asset.call_number cn
                     JOIN asset.copy cp ON (cp.call_number = cn.id)
-                    JOIN actor.org_unit a ON (cp.circ_lib = a.id)
               WHERE NOT cn.deleted
                     AND NOT cp.deleted
                     AND cp.circ_lib IN ( SELECT * FROM search.explode_array( search_org_list ) )
@@ -246,14 +277,25 @@ BEGIN
             IF NOT FOUND THEN
 
                 PERFORM 1
-                  FROM  asset.call_number cn
-                  WHERE cn.record IN ( SELECT * FROM search.explode_array( core_result.records ) )
+                  FROM  biblio.peer_bib_copy_map pr
+                        JOIN asset.copy cp ON (cp.id = pr.target_copy)
+                  WHERE NOT cp.deleted
+                        AND cp.circ_lib IN ( SELECT * FROM search.explode_array( search_org_list ) )
+                        AND pr.peer_record IN ( SELECT * FROM search.explode_array( core_result.records ) )
                   LIMIT 1;
 
-                IF FOUND THEN
-                    -- RAISE NOTICE ' % were all visibility-excluded ... ', core_result.records;
-                    excluded_count := excluded_count + 1;
-                    CONTINUE;
+                IF NOT FOUND THEN
+
+                    PERFORM 1
+                      FROM  asset.call_number cn
+                      WHERE cn.record IN ( SELECT * FROM search.explode_array( core_result.records ) )
+                      LIMIT 1;
+
+                    IF FOUND THEN
+                        -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
+                        excluded_count := excluded_count + 1;
+                        CONTINUE;
+                    END IF;
                 END IF;
 
             END IF;
index 549d034..3734a16 100644 (file)
@@ -7,6 +7,14 @@ INSERT INTO config.bib_source (id, quality, source, transcendant) VALUES
     (3, 1, oils_i18n_gettext(3, 'Project Gutenberg', 'cbs', 'source'), TRUE);
 SELECT SETVAL('config.bib_source_id_seq'::TEXT, 100);
 
+INSERT INTO biblio.peer_type (id,name) VALUES
+    (1,oils_i18n_gettext(1,'Bound Volume','bpt','name')),
+    (2,oils_i18n_gettext(2,'Bilingual','bpt','name')),
+    (3,oils_i18n_gettext(3,'Back-to-back','bpt','name')),
+    (4,oils_i18n_gettext(4,'Set','bpt','name')),
+    (5,oils_i18n_gettext(5,'e-Reader Preload','bpt','name')); 
+SELECT SETVAL('biblio.peer_type_id_seq'::TEXT, 100);
+
 INSERT INTO config.standing (id, value) VALUES (1, oils_i18n_gettext(1, 'Good', 'cst', 'value'));
 INSERT INTO config.standing (id, value) VALUES (2, oils_i18n_gettext(2, 'Barred', 'cst', 'value'));
 SELECT SETVAL('config.standing_id_seq'::TEXT, 100);
index 46019f2..ae69074 100644 (file)
@@ -281,6 +281,17 @@ CREATE OR REPLACE FUNCTION unapi.holdings_xml (bid BIGINT, ouid INT, org TEXT, d
                               WHERE record_entry = $1
                         )x)
                      )
+                 ELSE NULL END,
+                 CASE WHEN ('acp' = ANY ($5)) THEN 
+                     XMLELEMENT(
+                         name foreign_copies,
+                         (SELECT XMLAGG(acp) FROM (
+                            SELECT  unapi.acp(p.target_copy,'xml','copy','{}'::TEXT[], $3, $4, $6, $7, FALSE)
+                              FROM  biblio.peer_bib_copy_map p
+                                    JOIN asset.copy c ON (p.target_copy = c.id)
+                              WHERE NOT c.deleted AND peer_record = $1
+                        )x)
+                     )
                  ELSE NULL END
              );
 $F$ LANGUAGE SQL;
@@ -616,6 +627,14 @@ CREATE OR REPLACE FUNCTION unapi.acp ( obj_id BIGINT, format TEXT,  ename TEXT,
                             ELSE NULL
                         END
                     ),
+                    XMLELEMENT( name foreign_records,
+                        CASE
+                            WHEN ('bre' = ANY ($4)) THEN
+                                XMLAGG((SELECT unapi.bre(peer_record,'marcxml','record','{}'::TEXT[], $5, $6, $7, $8, FALSE) FROM biblio.peer_bib_copy_map WHERE target_copy = cp.id))
+                            ELSE NULL
+                        END
+
+                    ),
                     CASE 
                         WHEN ('bmp' = ANY ($4)) THEN
                             XMLELEMENT( name monograph_parts,
index d621300..30cf0e8 100644 (file)
@@ -1128,7 +1128,7 @@ CREATE OR REPLACE FUNCTION asset.refresh_opac_visible_copies_mat_view () RETURNS
 
     TRUNCATE TABLE asset.opac_visible_copies;
 
-    INSERT INTO asset.opac_visible_copies (id, circ_lib, record)
+    INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
     SELECT  cp.id, cp.circ_lib, cn.record
     FROM  asset.copy cp
         JOIN asset.call_number cn ON (cn.id = cp.call_number)
@@ -1142,6 +1142,18 @@ CREATE OR REPLACE FUNCTION asset.refresh_opac_visible_copies_mat_view () RETURNS
         AND cs.opac_visible
         AND cl.opac_visible
         AND cp.opac_visible
+        AND a.opac_visible
+            UNION
+    SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record
+    FROM  asset.copy cp
+        JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
+        JOIN actor.org_unit a ON (cp.circ_lib = a.id)
+        JOIN asset.copy_location cl ON (cp.location = cl.id)
+        JOIN config.copy_status cs ON (cp.status = cs.id)
+    WHERE NOT cp.deleted
+        AND cs.opac_visible
+        AND cl.opac_visible
+        AND cp.opac_visible
         AND a.opac_visible;
 
 $$ LANGUAGE SQL;
@@ -1157,8 +1169,9 @@ DECLARE
     do_remove       BOOLEAN := false;
 BEGIN
     add_query := $$
-            INSERT INTO asset.opac_visible_copies (id, circ_lib, record)
-                SELECT  cp.id, cp.circ_lib, cn.record
+            INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
+              SELECT id, circ_lib, record FROM (
+                SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number
                   FROM  asset.copy cp
                         JOIN asset.call_number cn ON (cn.id = cp.call_number)
                         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
@@ -1172,14 +1185,40 @@ BEGIN
                         AND cl.opac_visible
                         AND cp.opac_visible
                         AND a.opac_visible
+                            UNION
+                SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number
+                  FROM  asset.copy cp
+                        JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
+                        JOIN actor.org_unit a ON (cp.circ_lib = a.id)
+                        JOIN asset.copy_location cl ON (cp.location = cl.id)
+                        JOIN config.copy_status cs ON (cp.status = cs.id)
+                  WHERE NOT cp.deleted
+                        AND cs.opac_visible
+                        AND cl.opac_visible
+                        AND cp.opac_visible
+                        AND a.opac_visible
+                    ) AS x 
+
     $$;
  
-    remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE id IN ( SELECT id FROM asset.copy WHERE $$;
+    remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
+
+    IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
+        IF TG_OP = 'INSERT' THEN
+            add_query := add_query || 'WHERE x.id = ' || NEW.target_copy || ' AND x.record = ' || NEW.peer_record || ';';
+            EXECUTE add_query;
+            RETURN NEW;
+        ELSE
+            remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
+            EXECUTE remove_query;
+            RETURN OLD;
+        END IF;
+    END IF;
 
     IF TG_OP = 'INSERT' THEN
 
         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
-            add_query := add_query || 'AND cp.id = ' || NEW.id || ';';
+            add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
             EXECUTE add_query;
         END IF;
 
@@ -1225,7 +1264,7 @@ BEGIN
             DELETE FROM asset.opac_visible_copies WHERE id = NEW.id;
         END IF;
         IF do_add THEN
-            add_query := add_query || 'AND cp.id = ' || NEW.id || ';';
+            add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
             EXECUTE add_query;
         END IF;
 
@@ -1252,11 +1291,11 @@ BEGIN
         ELSIF OLD.deleted THEN -- add rows
  
             IF TG_TABLE_NAME IN ('copy','unit') THEN
-                add_query := add_query || 'AND cp.id = ' || NEW.id || ';';
+                add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
             ELSIF TG_TABLE_NAME = 'call_number' THEN
-                add_query := add_query || 'AND cp.call_number = ' || NEW.id || ';';
+                add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
             ELSIF TG_TABLE_NAME = 'record_entry' THEN
-                add_query := add_query || 'AND cn.record = ' || NEW.id || ';';
+                add_query := add_query || 'WHERE x.record = ' || NEW.id || ';';
             END IF;
  
             EXECUTE add_query;
@@ -1272,7 +1311,7 @@ BEGIN
             -- call number is linked to different bib
             remove_query := remove_query || 'call_number = ' || NEW.id || ');';
             EXECUTE remove_query;
-            add_query := add_query || 'AND cp.call_number = ' || NEW.id || ';';
+            add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
             EXECUTE add_query;
         END IF;
 
@@ -1321,6 +1360,7 @@ $func$ LANGUAGE PLPGSQL;
 COMMENT ON FUNCTION asset.cache_copy_visibility() IS $$
 Trigger function to update the copy OPAC visiblity cache.
 $$;
+CREATE TRIGGER a_opac_vis_mat_view_tgr AFTER INSERT OR DELETE ON biblio.peer_bib_copy_map FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
 CREATE TRIGGER a_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON biblio.record_entry FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
 CREATE TRIGGER a_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON asset.copy FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
 CREATE TRIGGER a_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON asset.call_number FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
diff --git a/Open-ILS/src/sql/Pg/upgrade/0512.schema.multi-home-items.sql b/Open-ILS/src/sql/Pg/upgrade/0512.schema.multi-home-items.sql
new file mode 100644 (file)
index 0000000..04816a7
--- /dev/null
@@ -0,0 +1,876 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0512'); -- miker
+
+CREATE TABLE biblio.peer_type (
+    id      SERIAL  PRIMARY KEY,
+    name        TEXT        NOT NULL UNIQUE -- i18n
+);
+
+CREATE TABLE biblio.peer_bib_copy_map (
+    id      SERIAL  PRIMARY KEY,
+    peer_type   INT     NOT NULL REFERENCES biblio.peer_type (id),
+    peer_record BIGINT      NOT NULL REFERENCES biblio.record_entry (id),
+    target_copy BIGINT      NOT NULL -- can't use fkey because of acp subtables
+);
+CREATE INDEX peer_bib_copy_map_record_idx ON biblio.peer_bib_copy_map (peer_record);
+CREATE INDEX peer_bib_copy_map_copy_idx ON biblio.peer_bib_copy_map (target_copy);
+
+DROP TABLE asset.opac_visible_copies;
+CREATE TABLE asset.opac_visible_copies (
+  id        BIGSERIAL primary key,
+  copy_id   BIGINT,
+  record    BIGINT,
+  circ_lib  INTEGER
+);
+
+INSERT INTO biblio.peer_type (id,name) VALUES
+    (1,oils_i18n_gettext(1,'Bound Volume','bpt','name')),
+    (2,oils_i18n_gettext(2,'Bilingual','bpt','name')),
+    (3,oils_i18n_gettext(3,'Back-to-back','bpt','name')),
+    (4,oils_i18n_gettext(4,'Set','bpt','name')),
+    (5,oils_i18n_gettext(5,'e-Reader Preload','bpt','name')); 
+
+SELECT SETVAL('biblio.peer_type_id_seq'::TEXT, 100);
+
+CREATE OR REPLACE FUNCTION search.query_parser_fts (
+
+    param_search_ou INT,
+    param_depth     INT,
+    param_query     TEXT,
+    param_statuses  INT[],
+    param_locations INT[],
+    param_offset    INT,
+    param_check     INT,
+    param_limit     INT,
+    metarecord      BOOL,
+    staff           BOOL
+) RETURNS SETOF search.search_result AS $func$
+DECLARE
+
+    current_res         search.search_result%ROWTYPE;
+    search_org_list     INT[];
+
+    check_limit         INT;
+    core_limit          INT;
+    core_offset         INT;
+    tmp_int             INT;
+
+    core_result         RECORD;
+    core_cursor         REFCURSOR;
+    core_rel_query      TEXT;
+
+    total_count         INT := 0;
+    check_count         INT := 0;
+    deleted_count       INT := 0;
+    visible_count       INT := 0;
+    excluded_count      INT := 0;
+
+BEGIN
+
+    check_limit := COALESCE( param_check, 1000 );
+    core_limit  := COALESCE( param_limit, 25000 );
+    core_offset := COALESCE( param_offset, 0 );
+
+    -- core_skip_chk := COALESCE( param_skip_chk, 1 );
+
+    IF param_search_ou > 0 THEN
+        IF param_depth IS NOT NULL THEN
+            SELECT array_accum(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou, param_depth );
+        ELSE
+            SELECT array_accum(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou );
+        END IF;
+    ELSIF param_search_ou < 0 THEN
+        SELECT array_accum(distinct org_unit) INTO search_org_list FROM actor.org_lasso_map WHERE lasso = -param_search_ou;
+    ELSIF param_search_ou = 0 THEN
+        -- reserved for user lassos (ou_buckets/type='lasso') with ID passed in depth ... hack? sure.
+    END IF;
+
+    OPEN core_cursor FOR EXECUTE param_query;
+
+    LOOP
+
+        FETCH core_cursor INTO core_result;
+        EXIT WHEN NOT FOUND;
+        EXIT WHEN total_count >= core_limit;
+
+        total_count := total_count + 1;
+
+        CONTINUE WHEN total_count NOT BETWEEN  core_offset + 1 AND check_limit + core_offset;
+
+        check_count := check_count + 1;
+
+        PERFORM 1 FROM biblio.record_entry b WHERE NOT b.deleted AND b.id IN ( SELECT * FROM search.explode_array( core_result.records ) );
+        IF NOT FOUND THEN
+            -- RAISE NOTICE ' % were all deleted ... ', core_result.records;
+            deleted_count := deleted_count + 1;
+            CONTINUE;
+        END IF;
+
+        PERFORM 1
+          FROM  biblio.record_entry b
+                JOIN config.bib_source s ON (b.source = s.id)
+          WHERE s.transcendant
+                AND b.id IN ( SELECT * FROM search.explode_array( core_result.records ) );
+
+        IF FOUND THEN
+            -- RAISE NOTICE ' % were all transcendant ... ', core_result.records;
+            visible_count := visible_count + 1;
+
+            current_res.id = core_result.id;
+            current_res.rel = core_result.rel;
+
+            tmp_int := 1;
+            IF metarecord THEN
+                SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
+            END IF;
+
+            IF tmp_int = 1 THEN
+                current_res.record = core_result.records[1];
+            ELSE
+                current_res.record = NULL;
+            END IF;
+
+            RETURN NEXT current_res;
+
+            CONTINUE;
+        END IF;
+
+        PERFORM 1
+          FROM  asset.call_number cn
+                JOIN asset.uri_call_number_map map ON (map.call_number = cn.id)
+                JOIN asset.uri uri ON (map.uri = uri.id)
+          WHERE NOT cn.deleted
+                AND cn.label = '##URI##'
+                AND uri.active
+                AND ( param_locations IS NULL OR array_upper(param_locations, 1) IS NULL )
+                AND cn.record IN ( SELECT * FROM search.explode_array( core_result.records ) )
+                AND cn.owning_lib IN ( SELECT * FROM search.explode_array( search_org_list ) )
+          LIMIT 1;
+
+        IF FOUND THEN
+            -- RAISE NOTICE ' % have at least one URI ... ', core_result.records;
+            visible_count := visible_count + 1;
+
+            current_res.id = core_result.id;
+            current_res.rel = core_result.rel;
+
+            tmp_int := 1;
+            IF metarecord THEN
+                SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
+            END IF;
+
+            IF tmp_int = 1 THEN
+                current_res.record = core_result.records[1];
+            ELSE
+                current_res.record = NULL;
+            END IF;
+
+            RETURN NEXT current_res;
+
+            CONTINUE;
+        END IF;
+
+        IF param_statuses IS NOT NULL AND array_upper(param_statuses, 1) > 0 THEN
+
+            PERFORM 1
+              FROM  asset.call_number cn
+                    JOIN asset.copy cp ON (cp.call_number = cn.id)
+              WHERE NOT cn.deleted
+                    AND NOT cp.deleted
+                    AND cp.status IN ( SELECT * FROM search.explode_array( param_statuses ) )
+                    AND cn.record IN ( SELECT * FROM search.explode_array( core_result.records ) )
+                    AND cp.circ_lib IN ( SELECT * FROM search.explode_array( search_org_list ) )
+              LIMIT 1;
+
+            IF NOT FOUND THEN
+                PERFORM 1
+                  FROM  biblio.peer_bib_copy_map pr
+                        JOIN asset.copy cp ON (cp.id = pr.target_copy)
+                  WHERE NOT cp.deleted
+                        AND cp.status IN ( SELECT * FROM search.explode_array( param_statuses ) )
+                        AND pr.peer_record IN ( SELECT * FROM search.explode_array( core_result.records ) )
+                        AND cp.circ_lib IN ( SELECT * FROM search.explode_array( search_org_list ) )
+                  LIMIT 1;
+
+                IF NOT FOUND THEN
+                -- RAISE NOTICE ' % and multi-home linked records were all status-excluded ... ', core_result.records;
+                    excluded_count := excluded_count + 1;
+                    CONTINUE;
+                END IF;
+            END IF;
+
+        END IF;
+
+        IF param_locations IS NOT NULL AND array_upper(param_locations, 1) > 0 THEN
+
+            PERFORM 1
+              FROM  asset.call_number cn
+                    JOIN asset.copy cp ON (cp.call_number = cn.id)
+              WHERE NOT cn.deleted
+                    AND NOT cp.deleted
+                    AND cp.location IN ( SELECT * FROM search.explode_array( param_locations ) )
+                    AND cn.record IN ( SELECT * FROM search.explode_array( core_result.records ) )
+                    AND cp.circ_lib IN ( SELECT * FROM search.explode_array( search_org_list ) )
+              LIMIT 1;
+
+            IF NOT FOUND THEN
+                PERFORM 1
+                  FROM  biblio.peer_bib_copy_map pr
+                        JOIN asset.copy cp ON (cp.id = pr.target_copy)
+                  WHERE NOT cp.deleted
+                        AND cp.location IN ( SELECT * FROM search.explode_array( param_locations ) )
+                        AND pr.peer_record IN ( SELECT * FROM search.explode_array( core_result.records ) )
+                        AND cp.circ_lib IN ( SELECT * FROM search.explode_array( search_org_list ) )
+                  LIMIT 1;
+
+                IF NOT FOUND THEN
+                    -- RAISE NOTICE ' % and multi-home linked records were all copy_location-excluded ... ', core_result.records;
+                    excluded_count := excluded_count + 1;
+                    CONTINUE;
+                END IF;
+            END IF;
+
+        END IF;
+
+        IF staff IS NULL OR NOT staff THEN
+
+            PERFORM 1
+              FROM  asset.opac_visible_copies
+              WHERE circ_lib IN ( SELECT * FROM search.explode_array( search_org_list ) )
+                    AND record IN ( SELECT * FROM search.explode_array( core_result.records ) )
+              LIMIT 1;
+
+            IF NOT FOUND THEN
+                PERFORM 1
+                  FROM  biblio.peer_bib_copy_map pr
+                        JOIN asset.opac_visible_copies cp ON (cp.copy_id = pr.target_copy)
+                  WHERE cp.circ_lib IN ( SELECT * FROM search.explode_array( search_org_list ) )
+                        AND pr.peer_record IN ( SELECT * FROM search.explode_array( core_result.records ) )
+                  LIMIT 1;
+
+                IF NOT FOUND THEN
+
+                    -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
+                    excluded_count := excluded_count + 1;
+                    CONTINUE;
+                END IF;
+            END IF;
+
+        ELSE
+
+            PERFORM 1
+              FROM  asset.call_number cn
+                    JOIN asset.copy cp ON (cp.call_number = cn.id)
+              WHERE NOT cn.deleted
+                    AND NOT cp.deleted
+                    AND cp.circ_lib IN ( SELECT * FROM search.explode_array( search_org_list ) )
+                    AND cn.record IN ( SELECT * FROM search.explode_array( core_result.records ) )
+              LIMIT 1;
+
+            IF NOT FOUND THEN
+
+                PERFORM 1
+                  FROM  biblio.peer_bib_copy_map pr
+                        JOIN asset.copy cp ON (cp.id = pr.target_copy)
+                  WHERE NOT cp.deleted
+                        AND cp.circ_lib IN ( SELECT * FROM search.explode_array( search_org_list ) )
+                        AND pr.peer_record IN ( SELECT * FROM search.explode_array( core_result.records ) )
+                  LIMIT 1;
+
+                IF NOT FOUND THEN
+
+                    PERFORM 1
+                      FROM  asset.call_number cn
+                      WHERE cn.record IN ( SELECT * FROM search.explode_array( core_result.records ) )
+                      LIMIT 1;
+
+                    IF FOUND THEN
+                        -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
+                        excluded_count := excluded_count + 1;
+                        CONTINUE;
+                    END IF;
+                END IF;
+
+            END IF;
+
+        END IF;
+
+        visible_count := visible_count + 1;
+
+        current_res.id = core_result.id;
+        current_res.rel = core_result.rel;
+
+        tmp_int := 1;
+        IF metarecord THEN
+            SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
+        END IF;
+
+        IF tmp_int = 1 THEN
+            current_res.record = core_result.records[1];
+        ELSE
+            current_res.record = NULL;
+        END IF;
+
+        RETURN NEXT current_res;
+
+        IF visible_count % 1000 = 0 THEN
+            -- RAISE NOTICE ' % visible so far ... ', visible_count;
+        END IF;
+
+    END LOOP;
+
+    current_res.id = NULL;
+    current_res.rel = NULL;
+    current_res.record = NULL;
+    current_res.total = total_count;
+    current_res.checked = check_count;
+    current_res.deleted = deleted_count;
+    current_res.visible = visible_count;
+    current_res.excluded = excluded_count;
+
+    CLOSE core_cursor;
+
+    RETURN NEXT current_res;
+
+END;
+$func$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION unapi.holdings_xml (bid BIGINT, ouid INT, org TEXT, depth INT DEFAULT NULL, includes TEXT[] DEFAULT NULL::TEXT[], slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE) RETURNS XML AS $F$
+     SELECT  XMLELEMENT(
+                 name holdings,
+                 XMLATTRIBUTES(
+                    CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                    CASE WHEN ('bre' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id
+                 ),
+                 XMLELEMENT(
+                     name counts,
+                     (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
+                         SELECT  XMLELEMENT(
+                                     name count,
+                                     XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+                                 )::text
+                           FROM  asset.opac_ou_record_copy_count($2,  $1)
+                                     UNION
+                         SELECT  XMLELEMENT(
+                                     name count,
+                                     XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+                                 )::text
+                           FROM  asset.staff_ou_record_copy_count($2, $1)
+                                     ORDER BY 1
+                     )x)
+                 ),
+                 CASE
+                     WHEN ('bmp' = ANY ($5)) THEN
+                        XMLELEMENT( name monograph_parts,
+                            XMLAGG((SELECT unapi.bmp( id, 'xml', 'monograph_part', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'bre'), 'holdings_xml'), $3, $4, $6, $7, FALSE) FROM biblio.monograph_part WHERE record = $1))
+                        )
+                     ELSE NULL
+                 END,
+                 CASE WHEN ('acn' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN
+                     XMLELEMENT(
+                         name volumes,
+                         (SELECT XMLAGG(acn) FROM (
+                            SELECT  unapi.acn(acn.id,'xml','volume',evergreen.array_remove_item_by_value(evergreen.array_remove_item_by_value('{acn,auri}'::TEXT[] || $5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE)
+                              FROM  asset.call_number acn
+                              WHERE acn.record = $1
+                                    AND EXISTS (
+                                        SELECT  1
+                                          FROM  asset.copy acp
+                                                JOIN actor.org_unit_descendants(
+                                                    $2,
+                                                    (COALESCE(
+                                                        $4,
+                                                        (SELECT aout.depth
+                                                          FROM  actor.org_unit_type aout
+                                                                JOIN actor.org_unit aou ON (aou.ou_type = aout.id AND aou.id = $2)
+                                                        )
+                                                    ))
+                                                ) aoud ON (acp.circ_lib = aoud.id)
+                                          LIMIT 1
+                                    )
+                              ORDER BY label_sortkey
+                              LIMIT $6
+                              OFFSET $7
+                         )x)
+                     )
+                 ELSE NULL END,
+                 CASE WHEN ('ssub' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN
+                     XMLELEMENT(
+                         name subscriptions,
+                         (SELECT XMLAGG(ssub) FROM (
+                            SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
+                              FROM  serial.subscription
+                              WHERE record_entry = $1
+                        )x)
+                     )
+                 ELSE NULL END,
+                 CASE WHEN ('acp' = ANY ($5)) THEN
+                     XMLELEMENT(
+                         name foreign_copies,
+                         (SELECT XMLAGG(acp) FROM (
+                            SELECT  unapi.acp(p.target_copy,'xml','copy','{}'::TEXT[], $3, $4, $6, $7, FALSE)
+                              FROM  biblio.peer_bib_copy_map p
+                                    JOIN asset.copy c ON (p.target_copy = c.id)
+                              WHERE NOT c.deleted AND peer_record = $1
+                        )x)
+                     )
+                 ELSE NULL END
+             );
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.acp ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
+        SELECT  XMLELEMENT(
+                    name copy,
+                    XMLATTRIBUTES(
+                        CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                        'tag:open-ils.org:U2@acp/' || id AS id,
+                        create_date, edit_date, copy_number, circulate, deposit,
+                        ref, holdable, deleted, deposit_amount, price, barcode,
+                        circ_modifier, circ_as_type, opac_visible
+                    ),
+                    unapi.ccs( status, $2, 'status', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
+                    unapi.acl( location, $2, 'location', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
+                    unapi.aou( circ_lib, $2, 'circ_lib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
+                    unapi.aou( circ_lib, $2, 'circlib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
+                    CASE WHEN ('acn' = ANY ($4)) THEN unapi.acn( call_number, $2, 'call_number', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) ELSE NULL END,
+                    XMLELEMENT( name copy_notes,
+                        CASE
+                            WHEN ('acpn' = ANY ($4)) THEN
+                                XMLAGG((SELECT unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) FROM asset.copy_note WHERE owning_copy = cp.id AND pub))
+                            ELSE NULL
+                        END
+                    ),
+                    XMLELEMENT( name statcats,
+                        CASE
+                            WHEN ('ascecm' = ANY ($4)) THEN
+                                XMLAGG((SELECT unapi.ascecm( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) FROM asset.stat_cat_entry_copy_map WHERE owning_copy = cp.id))
+                            ELSE NULL
+                        END
+                    ),
+                    XMLELEMENT( name foreign_records,
+                        CASE
+                            WHEN ('bre' = ANY ($4)) THEN
+                                XMLAGG((SELECT unapi.bre(peer_record,'marcxml','record','{}'::TEXT[], $5, $6, $7, $8, FALSE) FROM biblio.peer_bib_copy_map WHERE target_copy = cp.id))
+                            ELSE NULL
+                        END
+
+                    ),
+                    CASE
+                        WHEN ('bmp' = ANY ($4)) THEN
+                            XMLELEMENT( name monograph_parts,
+                                XMLAGG((SELECT unapi.bmp( part, 'xml', 'monograph_part', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) FROM asset.copy_part_map WHERE target_copy = cp.id))
+                            )
+                        ELSE NULL
+                    END
+                )
+          FROM  asset.copy cp
+          WHERE id = $1
+          GROUP BY id, status, location, circ_lib, call_number, create_date, edit_date, copy_number, circulate, deposit, ref, holdable, deleted, deposit_amount, price, barcode, circ_modifier, circ_as_type, opac_visible;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION asset.refresh_opac_visible_copies_mat_view () RETURNS VOID AS $$
+
+    TRUNCATE TABLE asset.opac_visible_copies;
+
+    INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
+    SELECT  cp.id, cp.circ_lib, cn.record
+    FROM  asset.copy cp
+        JOIN asset.call_number cn ON (cn.id = cp.call_number)
+        JOIN actor.org_unit a ON (cp.circ_lib = a.id)
+        JOIN asset.copy_location cl ON (cp.location = cl.id)
+        JOIN config.copy_status cs ON (cp.status = cs.id)
+        JOIN biblio.record_entry b ON (cn.record = b.id)
+    WHERE NOT cp.deleted
+        AND NOT cn.deleted
+        AND NOT b.deleted
+        AND cs.opac_visible
+        AND cl.opac_visible
+        AND cp.opac_visible
+        AND a.opac_visible
+            UNION
+    SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record
+    FROM  asset.copy cp
+        JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
+        JOIN actor.org_unit a ON (cp.circ_lib = a.id)
+        JOIN asset.copy_location cl ON (cp.location = cl.id)
+        JOIN config.copy_status cs ON (cp.status = cs.id)
+    WHERE NOT cp.deleted
+        AND cs.opac_visible
+        AND cl.opac_visible
+        AND cp.opac_visible
+        AND a.opac_visible;
+
+$$ LANGUAGE SQL;
+COMMENT ON FUNCTION asset.refresh_opac_visible_copies_mat_view() IS $$
+Rebuild the copy OPAC visibility cache.  Useful during migrations.
+$$;
+
+SELECT asset.refresh_opac_visible_copies_mat_view();
+CREATE INDEX opac_visible_copies_idx1 on asset.opac_visible_copies (record, circ_lib);
+CREATE INDEX opac_visible_copies_copy_id_idx on asset.opac_visible_copies (copy_id);
+CREATE UNIQUE INDEX opac_visible_copies_once_per_record_idx on asset.opac_visible_copies (copy_id, record);
+CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
+DECLARE
+    add_query       TEXT;
+    remove_query    TEXT;
+    do_add          BOOLEAN := false;
+    do_remove       BOOLEAN := false;
+BEGIN
+    add_query := $$
+            INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
+              SELECT id, circ_lib, record FROM (
+                SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number
+                  FROM  asset.copy cp
+                        JOIN asset.call_number cn ON (cn.id = cp.call_number)
+                        JOIN actor.org_unit a ON (cp.circ_lib = a.id)
+                        JOIN asset.copy_location cl ON (cp.location = cl.id)
+                        JOIN config.copy_status cs ON (cp.status = cs.id)
+                        JOIN biblio.record_entry b ON (cn.record = b.id)
+                  WHERE NOT cp.deleted
+                        AND NOT cn.deleted
+                        AND NOT b.deleted
+                        AND cs.opac_visible
+                        AND cl.opac_visible
+                        AND cp.opac_visible
+                        AND a.opac_visible
+                            UNION
+                SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number
+                  FROM  asset.copy cp
+                        JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
+                        JOIN actor.org_unit a ON (cp.circ_lib = a.id)
+                        JOIN asset.copy_location cl ON (cp.location = cl.id)
+                        JOIN config.copy_status cs ON (cp.status = cs.id)
+                  WHERE NOT cp.deleted
+                        AND cs.opac_visible
+                        AND cl.opac_visible
+                        AND cp.opac_visible
+                        AND a.opac_visible
+                    ) AS x 
+
+    $$;
+    remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
+
+    IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
+        IF TG_OP = 'INSERT' THEN
+            add_query := add_query || 'WHERE x.id = ' || NEW.target_copy || ' AND x.record = ' || NEW.peer_record || ';';
+            EXECUTE add_query;
+            RETURN NEW;
+        ELSE
+            remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
+            EXECUTE remove_query;
+            RETURN OLD;
+        END IF;
+    END IF;
+
+    IF TG_OP = 'INSERT' THEN
+
+        IF TG_TABLE_NAME IN ('copy', 'unit') THEN
+            add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
+            EXECUTE add_query;
+        END IF;
+
+        RETURN NEW;
+
+    END IF;
+
+    -- handle items first, since with circulation activity
+    -- their statuses change frequently
+    IF TG_TABLE_NAME IN ('copy', 'unit') THEN
+
+        IF OLD.location    <> NEW.location OR
+           OLD.call_number <> NEW.call_number OR
+           OLD.status      <> NEW.status OR
+           OLD.circ_lib    <> NEW.circ_lib THEN
+            -- any of these could change visibility, but
+            -- we'll save some queries and not try to calculate
+            -- the change directly
+            do_remove := true;
+            do_add := true;
+        ELSE
+
+            IF OLD.deleted <> NEW.deleted THEN
+                IF NEW.deleted THEN
+                    do_remove := true;
+                ELSE
+                    do_add := true;
+                END IF;
+            END IF;
+
+            IF OLD.opac_visible <> NEW.opac_visible THEN
+                IF OLD.opac_visible THEN
+                    do_remove := true;
+                ELSIF NOT do_remove THEN -- handle edge case where deleted item
+                                        -- is also marked opac_visible
+                    do_add := true;
+                END IF;
+            END IF;
+
+        END IF;
+
+        IF do_remove THEN
+            DELETE FROM asset.opac_visible_copies WHERE id = NEW.id;
+        END IF;
+        IF do_add THEN
+            add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
+            EXECUTE add_query;
+        END IF;
+
+        RETURN NEW;
+
+    END IF;
+
+    IF TG_TABLE_NAME IN ('call_number', 'record_entry') THEN -- these have a 'deleted' column
+        IF OLD.deleted AND NEW.deleted THEN -- do nothing
+
+            RETURN NEW;
+        ELSIF NEW.deleted THEN -- remove rows
+            IF TG_TABLE_NAME = 'call_number' THEN
+                DELETE FROM asset.opac_visible_copies WHERE id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
+            ELSIF TG_TABLE_NAME = 'record_entry' THEN
+                DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
+            END IF;
+            RETURN NEW;
+        ELSIF OLD.deleted THEN -- add rows
+            IF TG_TABLE_NAME IN ('copy','unit') THEN
+                add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
+            ELSIF TG_TABLE_NAME = 'call_number' THEN
+                add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
+            ELSIF TG_TABLE_NAME = 'record_entry' THEN
+                add_query := add_query || 'WHERE x.record = ' || NEW.id || ';';
+            END IF;
+            EXECUTE add_query;
+            RETURN NEW;
+        END IF;
+    END IF;
+
+    IF TG_TABLE_NAME = 'call_number' THEN
+
+        IF OLD.record <> NEW.record THEN
+            -- call number is linked to different bib
+            remove_query := remove_query || 'call_number = ' || NEW.id || ');';
+            EXECUTE remove_query;
+            add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
+            EXECUTE add_query;
+        END IF;
+
+        RETURN NEW;
+
+    END IF;
+
+    IF TG_TABLE_NAME IN ('record_entry') THEN
+        RETURN NEW; -- don't have 'opac_visible'
+    END IF;
+
+    -- actor.org_unit, asset.copy_location, asset.copy_status
+    IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
+
+        RETURN NEW;
+
+    ELSIF NEW.opac_visible THEN -- add rows
+
+        IF TG_TABLE_NAME = 'org_unit' THEN
+            add_query := add_query || 'AND cp.circ_lib = ' || NEW.id || ';';
+        ELSIF TG_TABLE_NAME = 'copy_location' THEN
+            add_query := add_query || 'AND cp.location = ' || NEW.id || ';';
+        ELSIF TG_TABLE_NAME = 'copy_status' THEN
+            add_query := add_query || 'AND cp.status = ' || NEW.id || ';';
+        END IF;
+        EXECUTE add_query;
+    ELSE -- delete rows
+
+        IF TG_TABLE_NAME = 'org_unit' THEN
+            remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
+        ELSIF TG_TABLE_NAME = 'copy_location' THEN
+            remove_query := remove_query || 'location = ' || NEW.id || ');';
+        ELSIF TG_TABLE_NAME = 'copy_status' THEN
+            remove_query := remove_query || 'status = ' || NEW.id || ');';
+        END IF;
+        EXECUTE remove_query;
+    END IF;
+    RETURN NEW;
+END;
+$func$ LANGUAGE PLPGSQL;
+COMMENT ON FUNCTION asset.cache_copy_visibility() IS $$
+Trigger function to update the copy OPAC visiblity cache.
+$$;
+
+CREATE TRIGGER a_opac_vis_mat_view_tgr AFTER INSERT OR DELETE ON biblio.peer_bib_copy_map FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
+
+CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
+DECLARE
+    transformed_xml TEXT;
+    prev_xfrm       TEXT;
+    normalizer      RECORD;
+    xfrm            config.xml_transform%ROWTYPE;
+    attr_value      TEXT;
+    new_attrs       HSTORE := ''::HSTORE;
+    attr_def        config.record_attr_definition%ROWTYPE;
+BEGIN
+
+    IF NEW.deleted IS TRUE THEN -- If this bib is deleted
+        DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id; -- Rid ourselves of the search-estimate-killing linkage
+        DELETE FROM metabib.record_attr WHERE id = NEW.id; -- Kill the attrs hash, useless on deleted records
+        DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
+        DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = NEW.id; -- Separate any multi-homed items
+        RETURN NEW; -- and we're done
+    END IF;
+
+    IF TG_OP = 'UPDATE' THEN -- re-ingest?
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
+
+        IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
+            RETURN NEW;
+        END IF;
+    END IF;
+
+    -- Record authority linking
+    PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
+    IF NOT FOUND THEN
+        PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
+    END IF;
+
+    -- Flatten and insert the mfr data
+    PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
+    IF NOT FOUND THEN
+        PERFORM metabib.reingest_metabib_full_rec(NEW.id);
+
+        -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
+        IF NOT FOUND THEN
+            FOR attr_def IN SELECT * FROM config.record_attr_definition ORDER BY format LOOP
+
+                IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
+                    SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(value), COALESCE(attr_def.joiner,' ')) INTO attr_value
+                      FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
+                      WHERE record = NEW.id
+                            AND tag LIKE attr_def.tag
+                            AND CASE
+                                WHEN attr_def.sf_list IS NOT NULL 
+                                    THEN POSITION(subfield IN attr_def.sf_list) > 0
+                                ELSE TRUE
+                                END
+                      GROUP BY tag
+                      ORDER BY tag
+                      LIMIT 1;
+
+                ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
+                    attr_value := biblio.marc21_extract_fixed_field(NEW.id, attr_def.fixed_field);
+
+                ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
+
+                    SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
+            
+                    -- See if we can skip the XSLT ... it's expensive
+                    IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
+                        -- Can't skip the transform
+                        IF xfrm.xslt <> '---' THEN
+                            transformed_xml := oils_xslt_process(NEW.marc,xfrm.xslt);
+                        ELSE
+                            transformed_xml := NEW.marc;
+                        END IF;
+            
+                        prev_xfrm := xfrm.name;
+                    END IF;
+
+                    IF xfrm.name IS NULL THEN
+                        -- just grab the marcxml (empty) transform
+                        SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
+                        prev_xfrm := xfrm.name;
+                    END IF;
+
+                    attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
+
+                ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
+                    SELECT  value::TEXT INTO attr_value
+                      FROM  biblio.marc21_physical_characteristics(NEW.id)
+                      WHERE subfield = attr_def.phys_char_sf
+                      LIMIT 1; -- Just in case ...
+
+                END IF;
+
+                -- apply index normalizers to attr_value
+                FOR normalizer IN
+                    SELECT  n.func AS func,
+                            n.param_count AS param_count,
+                            m.params AS params
+                      FROM  config.index_normalizer n
+                            JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
+                      WHERE attr = attr_def.name
+                      ORDER BY m.pos LOOP
+                        EXECUTE 'SELECT ' || normalizer.func || '(' ||
+                            quote_literal( attr_value ) ||
+                            CASE
+                                WHEN normalizer.param_count > 0
+                                    THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
+                                    ELSE ''
+                                END ||
+                            ')' INTO attr_value;
+        
+                END LOOP;
+
+                -- Add the new value to the hstore
+                new_attrs := new_attrs || hstore( attr_def.name, attr_value );
+
+            END LOOP;
+
+            IF TG_OP = 'INSERT' OR OLD.deleted THEN -- initial insert OR revivication
+                INSERT INTO metabib.record_attr (id, attrs) VALUES (NEW.id, new_attrs);
+            ELSE
+                UPDATE metabib.record_attr SET attrs = attrs || new_attrs WHERE id = NEW.id;
+            END IF;
+
+        END IF;
+    END IF;
+
+    -- Gather and insert the field entry data
+    PERFORM metabib.reingest_metabib_field_entries(NEW.id);
+
+    -- Located URI magic
+    IF TG_OP = 'INSERT' THEN
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
+        IF NOT FOUND THEN
+            PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
+        END IF;
+    ELSE
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
+        IF NOT FOUND THEN
+            PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
+        END IF;
+    END IF;
+
+    -- (re)map metarecord-bib linking
+    IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
+        IF NOT FOUND THEN
+            PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
+        END IF;
+    ELSE -- we're doing an update, and we're not deleted, remap
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
+        IF NOT FOUND THEN
+            PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
+        END IF;
+    END IF;
+
+    RETURN NEW;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+COMMIT;
index 5e2b66b..287a126 100644 (file)
@@ -381,8 +381,10 @@ var FETCH_MR_DESCRIPTORS           = 'open-ils.search:open-ils.search.metabib.record_to_d
 var FETCH_HIGHEST_PERM_ORG             = 'open-ils.actor:open-ils.actor.user.perm.highest_org.batch';
 var FETCH_USER_NOTES                           = 'open-ils.actor:open-ils.actor.note.retrieve.all';
 var FETCH_ORG_BY_SHORTNAME             = 'open-ils.actor:open-ils.actor.org_unit.retrieve_by_shortname';
-var FETCH_BIB_ID_BY_BARCODE = 'open-ils.search:open-ils.search.bib_id.by_barcode';
+var FETCH_BIB_IDS_BY_BARCODE = 'open-ils.search:open-ils.search.multi_home.bib_ids.by_barcode';
 var FETCH_ORG_SETTING = 'open-ils.actor:open-ils.actor.ou_setting.ancestor_default';
+var TEST_PEER_BIBS                             = 'open-ils.search:open-ils.search.peer_bibs.test';
+var FETCH_PEER_BIBS                            = 'open-ils.search:open-ils.search.peer_bibs';
 
 /* ---------------------------------------------------------------------------- */
 
index e8bb029..ef4c6cc 100644 (file)
 <!ENTITY staff.cat.opac.default.label "Set bottom interface as Default">
 <!ENTITY staff.cat.opac.manage_parts.accesskey "P">
 <!ENTITY staff.cat.opac.manage_parts.label "Manage Parts">
+<!ENTITY staff.cat.opac.manage_multi_home_items.accesskey "F">
+<!ENTITY staff.cat.opac.manage_multi_home_items.label "Manage Foreign Items">
+<!ENTITY staff.cat.opac.mark_for_multi_home.accesskey "F">
+<!ENTITY staff.cat.opac.mark_for_multi_home.label "Mark as Target for Foreign Items">
 <!ENTITY staff.cat.opac.marc_edit.accesskey "E">
 <!ENTITY staff.cat.opac.marc_edit.label "MARC Edit">
 <!ENTITY staff.cat.opac.marc_view.accesskey "V">
 <!ENTITY staff.hold_list.cancel_hold_dialog.cancel_btn.accesskey "C">
 <!ENTITY staff.hold_list.cancel_hold_dialog.apply_btn.label "Apply">
 <!ENTITY staff.hold_list.cancel_hold_dialog.apply_btn.accesskey "A">
+<!ENTITY staff.cat.manage_multi_bib_items.caption "Manage Foreign Items">
+<!ENTITY staff.cat.manage_multi_bib_items.actions.menu.label "Actions for Selected Items">
+<!ENTITY staff.cat.manage_multi_bib_items.actions.menu.accesskey "f">
+<!ENTITY staff.cat.manage_multi_bib_items.actions.menu_entry.show_in_opac.label "Show in Catalog">
+<!ENTITY staff.cat.manage_multi_bib_items.actions.menu_entry.show_in_opac.accesskey "S">
+<!ENTITY staff.cat.manage_multi_bib_items.actions.menu_entry.change_peer_type.label "Change Peer Type">
+<!ENTITY staff.cat.manage_multi_bib_items.actions.menu_entry.change_peer_type.accesskey "C">
+<!ENTITY staff.cat.manage_multi_bib_items.actions.menu_entry.unlink_from_bib.label "Remove from Bib">
+<!ENTITY staff.cat.manage_multi_bib_items.actions.menu_entry.unlink_from_bib.accesskey "R">
+<!ENTITY staff.cat.manage_multi_bib_items.peer_type.menu.label "Peer Type:">
+<!ENTITY staff.cat.manage_multi_bib_items.peer_type.menu.accesskey "T">
+<!ENTITY staff.cat.manage_multi_bib_items.barcode.textbox.label "Barcode:">
+<!ENTITY staff.cat.manage_multi_bib_items.barcode.textbox.accesskey "B">
+<!ENTITY staff.cat.manage_multi_bib_items.barcode.submit.label "Link to Bib (Submit)">
+<!ENTITY staff.cat.manage_multi_bib_items.barcode.submit.accesskey "S">
 <!ENTITY staff.cat.copy_browser.actions.sel_clip.label "Copy to Clipboard">
 <!ENTITY staff.cat.copy_browser.actions.sel_clip.accesskey "C">
 <!ENTITY staff.cat.copy_browser.actions.cmd_add_items_to_buckets.label "Add Items to Buckets">
 <!ENTITY staff.cat.copy_browser.actions.cmd_edit_items.accesskey "E">
 <!ENTITY staff.cat.copy_browser.actions.cmd_transfer_items.label "Transfer Items to Previously Marked Volume">
 <!ENTITY staff.cat.copy_browser.actions.cmd_transfer_items.accesskey "T">
+<!ENTITY staff.cat.copy_browser.actions.cmd_link_as_multi_bib.label "Link as Foreign Items to Previously Marked Bib Record">
+<!ENTITY staff.cat.copy_browser.actions.cmd_link_as_multi_bib.accesskey "F">
 <!ENTITY staff.cat.copy_browser.actions.cmd_add_volumes.label "Add Volumes">
 <!ENTITY staff.cat.copy_browser.actions.cmd_add_volumes.accesskey "V">
 <!ENTITY staff.cat.copy_browser.actions.cmd_mark_library.label "Mark Library as Volume Transfer Destination">
index c1ebf6b..b52813a 100644 (file)
@@ -500,6 +500,7 @@ Please see a librarian to renew your account.">
 <!ENTITY rdetail.cn.less "less info">
 <!ENTITY rdetail.cn.hold "place hold">
 <!ENTITY rdetail.cn.reserve "book now">
+<!ENTITY rdetail.cn.multi_home "linked titles">
 <!ENTITY rdetail.cn.disabled "- Disabled -">
 <!ENTITY rdetail.cn.note "Copy Note">
 <!ENTITY rdetail.cn.category "Copy Category">
@@ -549,6 +550,7 @@ We recommend that you remove this title from any bookbags it may have been added
 <!ENTITY rdetail.extras.author.notes "Author Notes">
 <!ENTITY rdetail.extras.annotation "Annotation">
 <!ENTITY rdetail.extras.marc "MARC Record">
+<!ENTITY rdetail.extras.foreign_items "Linked Titles">
 <!ENTITY rdetail.extras.call.null "There are no call numbers for this item at this location.">
 <!ENTITY rdetail.extras.call.local "Local Call Numbers:">
 <!ENTITY rdetail.extras.preview.fulltext "Full text">
index 12728c1..0e898e4 100644 (file)
@@ -149,22 +149,28 @@ function advGenericSearch() {
 
 
 function advFindBarcode(barcode) {
-    var req = new Request(FETCH_BIB_ID_BY_BARCODE, barcode);
+    var req = new Request(FETCH_BIB_IDS_BY_BARCODE, barcode);
     req.callback(advDrawBarcode);
     req.request.alertEvent = false;
     req.send();
 }
 
 function advDrawBarcode(r) {
-    titleid = r.getResultObject();
-    if(checkILSEvent(titleid)) {
+    var title_ids = r.getResultObject();
+    if(checkILSEvent(title_ids)) {
         alertId('myopac.copy.not.found');
         return;
     }
-    if(!titleid) return;
+    if(!title_ids) return;
     var args = {};
-    args.page = RDETAIL;
-    args[PARAM_RID] = titleid;
+    if (title_ids.length == 1) {
+        args.page = RDETAIL;
+        args[PARAM_RID] = title_ids[0];
+    } else {
+        args.page = RRESULT;
+           args[PARAM_RTYPE] = RTYPE_LIST;
+           args[PARAM_RLIST] = title_ids;
+    }
     location.href = buildOPACLink(args);
 }
 
index 04e8e41..50361b6 100644 (file)
@@ -9,7 +9,7 @@ var showDueDate = false;
 */
 var showDueTime = false;
 
-function cpdBuild( contextTbody, contextRow, record, callnumber, orgid, depth, copy_location ) {
+function cpdBuild( contextTbody, contextRow, record, callnumber, orgid, depth, copy_location, already_fetched_copies, peer_types ) {
        var i = cpdCheckExisting(contextRow);
        if(i) return i;
 
@@ -43,33 +43,51 @@ function cpdBuild( contextTbody, contextRow, record, callnumber, orgid, depth, c
        var print = $n(templateRow,'print');
        print.onclick = function() { cpdBuildPrintPane(
                contextRow, record, callnumber, orgid, depth) };
+    if (typeof callnumber == 'object') {
+        addCSSClass(print,'hide_me');
+    }
 
        var mainTbody = $n(templateRow, 'copies_tbody');
        var extrasRow = mainTbody.removeChild($n(mainTbody, 'copy_extras_row'));
 
-       var req = new Request(FETCH_COPIES_FROM_VOLUME, record.doc_id(), callnumber, orgid);
-       req.callback(cpdDrawCopies);
+    var request_args = {
+        peer_types      : peer_types, /* indexed the same as already_fetched_copies */
+        contextTbody   : contextTbody, /* tbody that holds the contextrow */
+        contextRow             : contextRow, /* the row our new row will be inserted after */
+        record                 : record,
+        callnumber             : callnumber, 
+        orgid                          : orgid,
+        depth                          : depth,
+        templateRow            : templateRow, /* contains everything */
+        copy_location          : copy_location,
+        mainTbody              : mainTbody, /* holds the copy rows */
+        extrasRow              : extrasRow, /* wrapper row for all extras */
+        counter                        : counter
+    }
 
-       req.request.args = { 
-               contextTbody    : contextTbody, /* tbody that holds the contextrow */
-               contextRow              : contextRow, /* the row our new row will be inserted after */
-               record                  : record,
-               callnumber              : callnumber, 
-               orgid                           : orgid,
-               depth                           : depth,
-               templateRow             : templateRow, /* contains everything */
-               copy_location           : copy_location,
-               mainTbody               : mainTbody, /* holds the copy rows */
-               extrasRow               : extrasRow, /* wrapper row for all extras */
-               counter                 : counter
-       };
+    if (! already_fetched_copies) {
+        var req = new Request(FETCH_COPIES_FROM_VOLUME, record.doc_id(), callnumber, orgid);
+        req.callback(cpdDrawCopies);
+
+        req.request.args = request_args;
+
+        req.send();
+    } else {
+        setTimeout(
+            function() {
+                delete request_args['copy_location'];
+                cpdDrawCopies({
+                    'args' : request_args,
+                    'getResultObject' : function() { return already_fetched_copies; }
+                });
+            }, 0
+        );
+    }
 
        if( contextRow.nextSibling ) 
                contextTbody.insertBefore( templateRow, contextRow.nextSibling );
        else
                contextTbody.appendChild( templateRow );
-
-       req.send();
        _debug('creating new details row with id ' + templateRow.id);
        cpdNodes[templateRow.id] = { templateRow : templateRow };
        return templateRow.id;
@@ -196,11 +214,27 @@ function cpdDrawCopies(r) {
        for( var i = 0; i < copies.length; i++ ) {
                var row = copyrow.cloneNode(true);
                var copyid = copies[i];
-               var req = new Request(FETCH_FLESHED_COPY, copies[i]);
-               req.callback(cpdDrawCopy);
-               req.request.args = r.args;
-               req.request.row = row;
-               req.send();
+        var pt; if (args.peer_types) pt = args.peer_types[i];
+        if (typeof copyid != 'object') {
+            var req = new Request(FETCH_FLESHED_COPY, copyid);
+            req.callback(cpdDrawCopy);
+            req.request.args = r.args;
+            req.request.row = row;
+            req.send();
+        } else {
+            setTimeout(
+                function(copy,row,pt) {
+                    return function() {
+                        cpdDrawCopy({
+                            'getResultObject' : function() { return copy; },
+                            'args' : r.args,
+                            'peer_type' : pt,
+                            'row' : row
+                        });
+                    };
+                }(copies[i],row,pt), 0
+            );
+        }
                copytbody.appendChild(row);
        }
 }
@@ -208,6 +242,7 @@ function cpdDrawCopies(r) {
 function cpdDrawCopy(r) {
        var copy = r.getResultObject();
        var row  = r.row;
+       var pt   = r.peer_type;
     var trow = r.args.templateRow;
 
     if (r.args.copy_location && copy.location().name() != r.args.copy_location) {
@@ -215,7 +250,13 @@ function cpdDrawCopy(r) {
         return;
     }
 
-       $n(row, 'barcode').appendChild(text(copy.barcode()));
+       var b = $n(row, 'barcode').appendChild(text(copy.barcode()));
+
+    /* show the peer type*/
+    if (pt) {
+        $n(row, 'barcode').appendChild(text(' :: ' + pt));
+    }
+
        $n(row, 'location').appendChild(text(copy.location().name()));
        $n(row, 'status').appendChild(text(copy.status().name()));
 
@@ -231,6 +272,20 @@ function cpdDrawCopy(r) {
         }
     }
 
+    /* show the other bibs link */
+    if (copy.peer_record_maps().length > 0) {
+        var l = $n(row, 'copy_multi_home');
+        unHideMe(l);
+        var link_args = {};
+        link_args.page = RRESULT;
+        link_args[PARAM_RTYPE] = RTYPE_LIST;
+        link_args[PARAM_RLIST] = new Array();
+        for (var i = 0; i < copy.peer_record_maps().length; i++) {
+            link_args[PARAM_RLIST].push( copy.peer_record_maps()[i].peer_record() );
+        }
+        l.setAttribute('href',buildOPACLink(link_args));
+    }
+
        if(isXUL()) {
                /* show the hold link */
                var l = $n(row, 'copy_hold_link');
index b1fc03b..4b2984d 100644 (file)
@@ -237,6 +237,64 @@ function rdetailViewMarc(r,id) {
        $('rdetail_view_marc_box').insertBefore(span, $('rdetail_view_marc_box').firstChild);
 }
 
+function rdetailForeignItems(r,id) {
+       hideMe($('rdetail_extras_loading'));
+    var tbody = $('rdetail_foreign_items_tbody');
+
+    var robj = r.getResultObject(); /* mvr list with foreign_copy_maps fleshed */
+
+    for (var i = 0; i < robj.length; i++) {
+        var args = {};
+        args.page = RDETAIL;
+        args[PARAM_OFFSET] = 0;
+        args[PARAM_RID] = robj[i].doc_id();
+        var row = elem('tr'); tbody.appendChild(row);
+        var td1 = elem('td'); row.appendChild(td1);
+        var title = elem(
+            'a',
+            {
+                'href' : buildOPACLink(args),
+                'class' : 'classic_link'
+            },
+            robj[i].title()
+        );
+        td1.appendChild(title);
+        var td2 = elem('td',{},robj[i].author()); row.appendChild(td2);
+        var td3 = elem('td'); row.appendChild(td3);
+        var details = elem(
+            'a',
+            {
+                'href' : 'javascript:void(0)',
+                'class' : 'classic_link'
+            },
+            'Copy Details'
+        );
+        details.onclick = function(idx,context_row){
+            return function() {
+                cpdBuild(
+                    tbody,
+                    context_row,
+                    robj[idx],
+                    null,
+                    1,
+                    0,
+                    1,
+                    dojo.map(
+                        robj[idx].foreign_copy_maps(),
+                        function(x){ return x.target_copy(); }
+                    ),
+                    dojo.map(
+                        robj[idx].foreign_copy_maps(),
+                        function(x){ return x.peer_type().name(); }
+                    )
+                );
+            };
+        }(i,row);
+        td3.appendChild(details);
+    }
+}
+
+
 
 function rdetailShowLocalCopies() {
        rdetailShowLocal = true;
@@ -566,6 +624,16 @@ function _rdetailDraw(r) {
     if (novelist && currentISBN) {
         unHideMe($('rdetail_novelist_link'));
     }
+
+    // Multi-Home / Foreign Items / Peer Bibs
+    var req = new Request( TEST_PEER_BIBS, record.doc_id() );
+    req.callback(function(r){
+        var test = r.getResultObject();
+        if (test == "1") {
+            unHideMe($('rdetail_foreign_items_link'));
+        }
+    }); 
+    req.send();
 }
 
 
@@ -635,6 +703,7 @@ function rdetailAddToBookbag() {
 
 
 var rdetailMarcFetched = false;
+var rdetailForeignItemsFetched = false;
 function rdetailShowExtra(type, args) {
 
        hideMe($('rdetail_copy_info_div'));
@@ -648,6 +717,7 @@ function rdetailShowExtra(type, args) {
        hideMe($('cn_browse'));
        hideMe($('rdetail_cn_browse_div'));
        hideMe($('rdetail_novelist_div'));
+       hideMe($('rdetail_foreign_items_div'));
        hideMe($('rdetail_notes_div'));
 
        removeCSSClass($('rdetail_copy_info_link'), 'rdetail_extras_selected');
@@ -661,6 +731,7 @@ function rdetailShowExtra(type, args) {
        removeCSSClass($('rdetail_annotation_link'), 'rdetail_extras_selected');
        removeCSSClass($('rdetail_viewmarc_link'), 'rdetail_extras_selected');
        removeCSSClass($('rdetail_novelist_link'), 'rdetail_extras_selected');
+       removeCSSClass($('rdetail_foreign_items_link'), 'rdetail_extras_selected');
 
        switch(type) {
 
@@ -716,6 +787,17 @@ function rdetailShowExtra(type, args) {
                        unHideMe($('rdetail_novelist_div')); 
                        break;
 
+               case "foreign_items": 
+                       addCSSClass($('rdetail_foreign_items_link'), 'rdetail_extras_selected');
+                       unHideMe($('rdetail_foreign_items_div')); 
+            if(rdetailForeignItemsFetched) return;
+                       unHideMe($('rdetail_extras_loading'));
+            rdetailForeignItemsFetched = true;
+                       var req = new Request( FETCH_PEER_BIBS, record.doc_id() );
+                       req.callback(rdetailForeignItems); 
+                       req.send();
+                       break;
+
                case 'cn':
                        addCSSClass($('rdetail_viewcn_link'), 'rdetail_extras_selected');
                        unHideMe($('rdetail_cn_browse_div'));
index 45a5c85..f937089 100644 (file)
@@ -32,6 +32,8 @@
                                                                                href='javascript:void(0);'>&rdetail.cn.hold;</a>
                                                                        <a class='hide_me classic_link copy_more_info' name='copy_reserve_link' 
                                                                                href='javascript:void(0);'>&rdetail.cn.reserve;</a>
+                                                                       <a class='hide_me classic_link copy_more_info' name='copy_multi_home'
+                                                                               href='javascript:void(0);'>&rdetail.cn.multi_home;</a>
                                                                </td>
 
                                                                <td name='status'> </td>
index 4a2b94d..4245824 100644 (file)
                                                class='classic_link'>&rdetail.extras.novelist;</a>
                                </td>
 
+                               <td id='rdetail_foreign_items_link' class='hide_me rdetail_extras_td'
+                                       style='padding-right: 15px; padding-left: 15px;' >
+                                       <a href='javascript:rdetailShowExtra("foreign_items");'
+                                               class='classic_link'>&rdetail.extras.foreign_items;</a>
+                               </td>
+
                        </tr>
                </thead>
        </table>
             </div>
         </div>
 
+               <div id='rdetail_foreign_items_div' class='rdetail_extras_div hide_me'>
+            <table width='100%' class='data_grid data_grid_center' id='rdetail_foreign_items_table'>
+                <thead>
+                    <tr> <td>&common.title;</td> <td>&common.authors;</td> <td>&nbsp;</td></tr>
+                </thead>
+                <tbody id='rdetail_foreign_items_tbody'>
+                </tbody>
+            </table>
+        </div>
                <div id='rdetail_cn_browse_div' style='text-align: center;' class='hide_me'>
 
                        <div id='cn_browse_none' class='hide_me color_4' style='width: 90%; text-align: center; margin: 10px;'>
index f675cbe..f73f071 100644 (file)
@@ -580,6 +580,27 @@ OpenILS.data.prototype = {
         this.chain.push(
             function() {
                 var f = gen_fm_retrieval_func(
+                    'bpt',
+                    [
+                        api.FM_BPT_PCRUD_SEARCH.app,
+                        api.FM_BPT_PCRUD_SEARCH.method,
+                        [ obj.session.key, {"id":{"!=":null}}, {"order_by":{"bpt":"id"}} ],
+                        false
+                    ]
+                );
+                try {
+                    f();
+                } catch(E) {
+                    var error = 'Error: ' + js2JSON(E);
+                    obj.error.sdump('D_ERROR',error);
+                    throw(E);
+                }
+            }
+        );
+
+        this.chain.push(
+            function() {
+                var f = gen_fm_retrieval_func(
                     'csp',
                     [
                         api.FM_CSP_PCRUD_SEARCH.app,
index befb693..8250cae 100644 (file)
         return url;
     }
 
+    function widget_prompt(node,args) {
+        // args is an object that may contain the following keys: title, desc, ok_label, ok_accesskey, cancel_label, cancel_accesskey, access, method
+        // access may contain 'property' or 'attribute' or 'method' for retrieving the value from the node
+        // if 'method', then the method key will reference a function that returns the value
+        try {
+            if (!node) { return false; }
+            if (!args) { args = {}; }
+            args[ 'widget' ] = node;
+
+            var url = location.protocol == 'chrome'
+                ? 'chrome://open_ils_staff_client/content/util/widget_prompt.xul'
+                : '/xul/server/util/widget_prompt.xul';
+
+            JSAN.use('util.window'); var win = new util.window();
+            var my_xulG = win.open(
+                url,
+                args.title || 'widget_prompt',
+                'chrome,modal',
+                args
+            );
+
+            if (my_xulG.status == 'incomplete') {
+                return false;
+            } else {
+                return my_xulG.value;
+            }
+        } catch(E) {
+            alert('Error in global_utils.js, widget_prompt(): ' + E);
+        }
+    }
index 678efd5..47b45ea 100644 (file)
@@ -3,6 +3,8 @@ var docid; var marc_html; var top_pane; var bottom_pane; var opac_frame; var opa
 var marc_view_reset = true;
 var marc_edit_reset = true;
 var copy_browser_reset = true;
+var manage_parts_reset = true;
+var manage_multi_home_reset = true;
 var hold_browser_reset = true;
 var serctrl_view_reset = true;
 
@@ -674,10 +676,20 @@ function mark_for_overlay() {
     g.data.stash('marked_record_mvr');
     if (g.data.marked_record_mvr) {
         alert(document.getElementById('offlineStrings').getFormattedString('cat.opac.record_marked_for_overlay.tcn.alert',[ g.data.marked_record_mvr.tcn() ]));
-        xulG.set_statusbar(1, $("offlineStrings").getFormattedString('staff.cat.z3950.marked_record_for_overlay_indicator.tcn.label',[g.data.marked_record_mvr.tcn()]) );
+        xulG.set_statusbar(
+            1,
+            $("offlineStrings").getFormattedString('staff.cat.z3950.marked_record_for_overlay_indicator.tcn.label',[g.data.marked_record_mvr.tcn()]),
+            $("offlineStrings").getFormattedString('staff.cat.z3950.marked_record_for_overlay_indicator.record_id.label',[g.data.marked_record]),
+            gen_statusbar_click_handler('marked_record')
+        );
     } else {
         alert(document.getElementById('offlineStrings').getFormattedString('cat.opac.record_marked_for_overlay.record_id.alert',[ g.data.marked_record  ]));
-        xulG.set_statusbar(1, $("offlineStrings").getFormattedString('staff.cat.z3950.marked_record_for_overlay_indicator.record_id.label',[g.data.marked_record]) );
+        xulG.set_statusbar(
+            1,
+            $("offlineStrings").getFormattedString('staff.cat.z3950.marked_record_for_overlay_indicator.record_id.label',[g.data.marked_record]),
+            '',
+            gen_statusbar_click_handler('marked_record')
+        );
     }
 }
 
@@ -694,10 +706,22 @@ function mark_for_hold_transfer() {
     g.data.stash('marked_record_for_hold_transfer_mvr');
     if (g.data.marked_record_mvr) {
         var m = $("offlineStrings").getFormattedString('staff.cat.opac.marked_record_for_hold_transfer_indicator.tcn.label',[g.data.marked_record_for_hold_transfer_mvr.tcn()]);
-        alert(m); xulG.set_statusbar(1, m );
+        alert(m);
+        xulG.set_statusbar(
+            3,
+            m,
+            '',
+            gen_statusbar_click_handler('marked_record_for_hold_transfer')
+        );
     } else {
         var m = $("offlineStrings").getFormattedString('staff.cat.opac.marked_record_for_hold_transfer_indicator.record_id.label',[g.data.marked_record_for_hold_transfer]);
-        alert(m); xulG.set_statusbar(1, m );
+        alert(m);
+        xulG.set_statusbar(
+            3,
+            m,
+            '',
+            gen_statusbar_click_handler('marked_record_for_hold_transfer')
+        );
     }
 }
 
@@ -764,6 +788,9 @@ function refresh_display(id) {
         marc_edit_reset = true;
         copy_browser_reset = true;
         hold_browser_reset = true;
+        manage_parts_reset = true;
+        manage_multi_home_reset = true;
+        serctrl_view_reset = true;
         while(top_pane.node.lastChild) top_pane.node.removeChild( top_pane.node.lastChild );
         var children = bottom_pane.node.childNodes;
         for (var i = 0; i < children.length; i++) {
@@ -845,17 +872,103 @@ function add_volumes() {
 
 function manage_parts() {
     try {
-        var title = document.getElementById('offlineStrings').getFormattedString('staff.cat.manage_parts.title', [docid]);
+        g.view = 'manage_parts';
         var loc = urls.XUL_BROWSER + "?url=" + window.escape(
             window.xulG.url_prefix(urls.CONIFY_MANAGE_PARTS) + '?r=' + docid
         );
-        var w = xulG.new_tab(
-            loc,
-            { 'tab_name' : title },
-            {}
-        );
+        if (manage_parts_reset) {
+            bottom_pane.reset_iframe( loc,{},xulG);
+            manage_parts_reset =false;
+        } else {
+            bottom_pane.set_iframe( loc,{},xulG);
+        }
+        opac_wrapper_set_help_context();
+        bottom_pane.get_contentWindow().addEventListener('load',opac_wrapper_set_help_context,false);
     } catch(E) {
         alert('Error in chrome/content/cat/opac.js, manage_parts(): ' + E);
     }
 }
 
+function manage_multi_home_items() {
+    try {
+        g.view = 'manage_multi_home';
+        var loc = window.xulG.url_prefix(urls.MANAGE_MULTI_HOME_ITEMS);
+        if (manage_multi_home_reset) {
+            bottom_pane.reset_iframe( loc,{},{'docid':docid,'no_bib_summary':true,'url_prefix':xulG.url_prefix,'new_tab':xulG.new_tab});
+            manage_multi_home_reset =false;
+        } else {
+            bottom_pane.set_iframe( loc,{},{'docid':docid,'no_bib_summary':true,'url_prefix':xulG.url_prefix,'new_tab':xulG.new_tab});
+        }
+        opac_wrapper_set_help_context();
+        bottom_pane.get_contentWindow().addEventListener('load',opac_wrapper_set_help_context,false);
+    } catch(E) {
+        alert('Error in chrome/content/cat/opac.js, manage_multi_home_items(): ' + E);
+    }
+}
+
+function mark_for_multi_home() {
+    g.data.marked_multi_home_record = docid;
+    g.data.stash('marked_multi_home_record');
+    var robj = g.network.simple_request('MODS_SLIM_RECORD_RETRIEVE.authoritative',[docid]);
+    if (typeof robj.ilsevent == 'undefined') {
+        g.data.marked_multi_home_record_mvr = robj;
+    } else {
+        g.data.marked_multi_home_record_mvr = null;
+        g.error.standard_unexpected_error_alert('in mark_for_multi_home',robj);
+    }
+    g.data.stash('marked_multi_home_record_mvr');
+
+    if (g.data.marked_multi_home_record_mvr) {
+        alert(document.getElementById('offlineStrings').getFormattedString('cat.opac.record_marked_for_multi_home.tcn.alert',[ g.data.marked_multi_home_record_mvr.tcn() ]));
+        xulG.set_statusbar(
+            2,
+            $("offlineStrings").getFormattedString('staff.cat.copy_browser.marked_record_for_multi_home_indicator.tcn.label',[g.data.marked_multi_home_record_mvr.tcn()]),
+            $("offlineStrings").getFormattedString('staff.cat.copy_browser.marked_record_for_multi_home_indicator.record_id.label',[g.data.marked_multi_home_record]),
+            gen_statusbar_click_handler('marked_multi_home_record')
+        );
+    } else {
+        alert(document.getElementById('offlineStrings').getFormattedString('cat.opac.record_marked_for_multi_home.record_id.alert',[ g.data.marked_multi_home_record ]));
+        xulG.set_statusbar(
+            2,
+            $("offlineStrings").getFormattedString('staff.cat.copy_browser.marked_record_for_multi_home_indicator.record_id.label',[g.data.marked_multi_home_record]),
+            '',
+            gen_statusbar_click_handler('marked_multi_home_record')
+        );
+    }
+}
+
+function gen_statusbar_click_handler(data_key) {
+    return function (ev) {
+
+        if (! g.data[data_key]) {
+            return;
+        }
+
+        if (ev.button == 0 /* left click, spawn opac */) {
+            var opac_url = xulG.url_prefix( urls.opac_rdetail ) + '?r=' + g.data[data_key];
+            var content_params = {
+                'session' : ses(),
+                'authtime' : ses('authtime'),
+                'opac_url' : opac_url,
+            };
+            xulG.new_tab(
+                xulG.url_prefix(urls.XUL_OPAC_WRAPPER),
+                {'tab_name':'Retrieving title...'},
+                content_params
+            );
+        }
+
+        if (ev.button == 2 /* right click, remove mark */) {
+            if ( window.confirm( document.getElementById('offlineStrings').getString('cat.opac.clear_statusbar') ) ) {
+                g.data[data_key] = null;
+                g.data.stash(data_key);
+                ev.target.setAttribute('label','');
+                if (ev.target.hasAttribute('tooltiptext')) {
+                    ev.target.removeAttribute('tooltiptext');
+                }
+            }
+        }
+    }
+}
+
+
index 9e8549d..b7d517f 100644 (file)
@@ -48,6 +48,8 @@
                 <menuitem label="&staff.cat.opac.marc_view.label;" accesskey="&staff.cat.opac.marc_view.accesskey;" id="marc_view" oncommand="set_marc_view();"/>
                 <menuitem label="&staff.cat.opac.marc_edit.label;" accesskey="&staff.cat.opac.marc_edit.accesskey;" id="marc_edit" oncommand="set_marc_edit();"/>
                 <menuitem label="&staff.cat.opac.copy_browse.label;" accesskey="&staff.cat.opac.copy_browse.accesskey;" id="copy_browse" oncommand="set_copy_browser();"/>
+                <menuitem label="&staff.cat.opac.manage_multi_home_items.label;" accesskey="&staff.cat.opac.manage_multi_home_items.accesskey;" id="manage_multi_home_items" oncommand="manage_multi_home_items();"/>
+                <menuitem label="&staff.cat.opac.manage_parts.label;" accesskey="&staff.cat.opac.manage_parts.accesskey;" id="manage_parts" oncommand="manage_parts();"/>
                 <menuitem label="&staff.cat.opac.view_holds.label;" accesskey="&staff.cat.opac.view_holds.accesskey;" id="view_holds" oncommand="set_hold_browser();"/>
                 <menuitem label="&staff.cat.opac.view_orders.label;" accesskey="&staff.cat.opac.view_orders.accesskey;" id="view_orders" oncommand="open_acq_orders();"/>
                 <menuseparator/>
                 <menuitem label="&staff.cat.copy_browser.holdings_maintenance.cmd_add_volumes.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.cmd_add_volumes.accesskey;" id="add_volumes" oncommand="add_volumes();"/>
                 <menuitem label="&staff.cat.opac.mark_for_hold_transfer.label;" accesskey="&staff.cat.opac.mark_for_hold_transfer.accesskey;" id="mark_for_hold_transfer" oncommand="mark_for_hold_transfer();"/>
                 <menuitem label="&staff.cat.opac.transfer_title_holds.label;" accesskey="&staff.cat.opac.transfer_title_holds.accesskey;" id="transfer_title_holds" oncommand="transfer_title_holds();"/>
-                <menuitem label="&staff.cat.opac.manage_parts.label;" accesskey="&staff.cat.opac.manage_parts.accesskey;" id="manage_parts" oncommand="manage_parts();"/>
+                <menuitem label="&staff.cat.opac.mark_for_multi_home.label;" accesskey="&staff.cat.opac.mark_for_multi_home.accesskey;" id="mark_for_multi_home" oncommand="mark_for_multi_home();"/>
                 <menuseparator/>
                 <menuitem label="&staff.cat.opac.bib_in_new_tab.label;" id="bib_in_new_tab" oncommand="bib_in_new_tab();"/>
                 <menuitem label="&staff.cat.opac.remove_me.label;" id="remove_me" oncommand="remove_me();"/>
                 <menuseparator/>
-                <menuitem label="&staff.cat.opac.default.label;" id="default" oncommand="set_default();"/>
-                <menuitem label="&staff.cat.opac.refresh_me.label;" id="refresh_me" oncommand="refresh_display(docid);"/>
-                <menuseparator/>
                 <menu id="mfhd_menu" label="&staff.serial.mfhd_menu.label;">
                     <menupopup id="mfhd_popup">
                         <menuitem id="mfhd_add" label="&staff.serial.mfhd_menu.add.label;"/>
@@ -77,6 +76,9 @@
                 <menuitem id="serctrl_view" label="&staff.serial.serctrl_view.label;" oncommand="set_serctrl_view();" />
                 <menuitem label="&staff.cat.opac.alt_serial.label;" accesskey="&staff.cat.opac.alt_serial.accesskey;" id="alt_serial" oncommand="open_alt_serial_mgmt();" />
                 <menuitem label="&staff.cat.opac.batch_receive.label;" accesskey="&staff.cat.opac.batch_receive.accesskey;" id="batch_receive" oncommand="batch_receive_in_new_tab();"/>
+                <menuseparator/>
+                <menuitem label="&staff.cat.opac.default.label;" id="default" oncommand="set_default();"/>
+                <menuitem label="&staff.cat.opac.refresh_me.label;" id="refresh_me" oncommand="refresh_display(docid);"/>
                 </menupopup>
                 </menu>
             </menubar>
index 802592d..56ed059 100644 (file)
@@ -203,6 +203,7 @@ var api = {
     'FM_AUSP_PCRUD_UPDATE' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.update.ausp', 'secure' : false },
     'FM_AUSP_UPDATE_NOTE' : { 'app' : 'open-ils.actor', 'method' : 'open-ils.actor.user.penalty.note.update' },
     'FM_BOOKING_CREATE_BRT_AND_BRSRC' : { 'app' : 'open-ils.booking', 'method' : 'open-ils.booking.create_brt_and_brsrc_from_copies' },
+    'FM_BPT_PCRUD_SEARCH' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.search.bpt.atomic', 'secure' : false },
     'FM_BRESV_RETRIEVE_VIA_PCRUD' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.search.bresv.atomic' },
     'FM_BRSRC_RETRIEVE_VIA_PCRUD' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.search.brsrc.atomic' },
     'FM_BRT_RETRIEVE_VIA_PCRUD' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.search.brt.atomic' },
@@ -380,6 +381,7 @@ var urls = {
     'AUDIO_event_ASSET_COPY_NOT_FOUND' : '/xul/server/skin/media/audio/redalert.wav',
 
     'AUTHORITY_MANAGE' : '/eg/cat/authority/list',
+    'MANAGE_MULTI_HOME_ITEMS' : '/xul/server/cat/manage_multi_home_items.xul',
     'XUL_AUTH_SIMPLE' : '/xul/server/main/simple_auth.xul',
     'XUL_BIB_BRIEF' : '/xul/server/cat/bib_brief.xul',
     'XUL_BIB_BRIEF_VERTICAL' : '/xul/server/cat/bib_brief_vertical.xul',
index 54ea790..b8b9f8a 100644 (file)
@@ -1724,9 +1724,26 @@ commands:
         content_params.url_prefix = function(url) { return obj.url_prefix(url); };
         content_params.network_meter = obj.network_meter;
         content_params.page_meter = obj.page_meter;
-        content_params.set_statusbar = function(slot,text) {
+        content_params.set_statusbar = function(slot,text,tooltiptext,click_handler) {
             var e = document.getElementById('statusbarpanel'+slot);
-            if (e) { e.setAttribute('label',text); }
+            if (e) {
+                var p = e.parentNode;
+                var sbp = document.createElement('statusbarpanel');
+                sbp.setAttribute('id','statusbarpanel'+slot);
+                p.replaceChild(sbp,e); // destroy and replace the statusbarpanel as a poor man's way of clearing event handlers
+
+                sbp.setAttribute('label',text);
+                if (tooltiptext) {
+                    sbp.setAttribute('tooltiptext',tooltiptext);
+                }
+                if (click_handler) {
+                    sbp.addEventListener(
+                        'click',
+                        click_handler,
+                        false
+                    );
+                }
+            }
         };
         content_params.chrome_xulG = xulG;
         content_params._data = xulG._data;
index 38f0ca3..18e7d1e 100644 (file)
@@ -22,10 +22,7 @@ util.exec.prototype = {
     'timer' : function(funcs,interval) {
         var obj = this;
 
-        if (obj._intervalId) {
-            obj.clear_timer();
-            window.removeEventListener('unload',obj.clear_timer,false); 
-        }
+        obj.clear_timer();
         var intervalId = window.setInterval(
             function() {
                 if (typeof obj.debug != 'undefined' && obj.debug) { dump('EXEC: ' + location.pathname + ': Running interval with id = ' + intervalId + '\n'); }
@@ -40,6 +37,13 @@ util.exec.prototype = {
         window.addEventListener('unload',obj.clear_timer,false); 
         return intervalId;
     },
+    'clear_timer' : function() {
+        var obj = this;
+        if (obj._intervalId) {
+            obj.clear_timer();
+            window.removeEventListener('unload',obj.clear_timer,false);
+        }
+    },
     // This executes a series of functions, but tries to give other events/functions a chance to
     // execute between each one.
     'chain' : function () {
diff --git a/Open-ILS/xul/staff_client/chrome/content/util/widget_prompt.js b/Open-ILS/xul/staff_client/chrome/content/util/widget_prompt.js
new file mode 100644 (file)
index 0000000..c51689b
--- /dev/null
@@ -0,0 +1,78 @@
+var xulG = {};
+var widget;
+
+function my_init() {
+    try {
+        netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+        if (typeof JSAN == 'undefined') { throw( "The JSAN library object is missing."); }
+        JSAN.errorLevel = "die"; // none, warn, or die
+        JSAN.addRepository('/xul/server/');
+        JSAN.use('util.error'); g.error = new util.error();
+        g.error.sdump('D_TRACE','my_init() for widget_prompt.xul');
+
+        widget = xul_param('widget',{'modal_xulG':true});
+        if (widget) {
+            $('widget_prompt_main').appendChild(widget);
+        }
+
+        var ok_label = xul_param('ok_label',{'modal_xulG':true}) || offlineStrings.getString('common.ok.label');
+        $('ok_btn').setAttribute('label',ok_label);
+
+        var ok_accesskey = xul_param('ok_accesskey',{'modal_xulG':true}) || offlineStrings.getString('common.ok.accesskey');
+        $('ok_btn').setAttribute('accesskey',ok_accesskey);
+
+        var cancel_label = xul_param('cancel_label',{'modal_xulG':true}) || offlineStrings.getString('common.cancel.label');
+        $('cancel_btn').setAttribute('label',cancel_label);
+
+        var cancel_accesskey = xul_param('cancel_accesskey',{'modal_xulG':true}) || offlineStrings.getString('common.cancel.accesskey');
+        $('cancel_btn').setAttribute('accesskey',cancel_accesskey);
+
+        var desc = xul_param('desc',{'modal_xulG':true});
+        if (desc) {
+            $('desc').appendChild( document.createTextNode( desc ) );
+        }
+
+        $('ok_btn').addEventListener('command',widget_save,false);
+        $('cancel_btn').addEventListener('command',function(ev) { window.close(); },false);
+
+        if (xul_param('title',{'modal_xulG':true})) {
+            try { window.title = xul_param('title',{'modal_xulG':true}); } catch(E) {}
+            try { document.title = xul_param('title',{'modal_xulG':true}); } catch(E) {}
+        }
+
+        xulG[ 'status' ] = 'incomplete';
+        update_modal_xulG(xulG);
+
+        try { widget.focus(); } catch(E) {}
+
+    } catch(E) {
+        alert('Error in widget_prompt.js, my_init(): ' + E);
+    }
+}
+
+function widget_save(ev) {
+    try {
+        if (widget) {
+            switch( xul_param('access',{'modal_xulG':true}) ) {
+                case 'method' :
+                    xulG[ 'value' ] = xulG[ 'method' ]();
+                break;
+                case 'attribute':
+                    xulG[ 'value' ] = widget.getAttribute('value');
+                break;
+                case 'property':
+                default:
+                    xulG[ 'value'  ] = widget.value;
+                break;
+            }
+        }
+        xulG[ 'status' ] = 'complete';
+
+        update_modal_xulG(xulG);
+
+        window.close();
+    } catch(E) {
+        alert('Error in widget_prompt.js, widget_save(): ' + E);
+    }
+}
+
diff --git a/Open-ILS/xul/staff_client/chrome/content/util/widget_prompt.xul b/Open-ILS/xul/staff_client/chrome/content/util/widget_prompt.xul
new file mode 100644 (file)
index 0000000..303a4c3
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<!-- Application: Evergreen Staff Client -->
+<!-- Screen: Example Template for remote xul -->
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- STYLESHEETS -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/patron_display.css" type="text/css"?>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- LOCALIZATION -->
+<!DOCTYPE window PUBLIC "" ""[
+    <!--#include virtual="/opac/locale/en-US/lang.dtd"-->
+]>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- OVERLAYS -->
+<?xul-overlay href="/xul/server/OpenILS/util_overlay.xul"?>
+
+<window id="widget_prompt_win" 
+    onload="try { my_init(); font_helper(); persist_helper( xul_param('title',{'modal_xulG':true}) ); } catch(E) { alert(E); }"
+    oils_persist="width height"
+    xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+    <!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+    <!-- BEHAVIOR -->
+        <script type="text/javascript">
+        var myPackageDir = 'open_ils_staff_client'; var IAMXUL = true; var g = {};
+    </script>
+        <scripts id="openils_util_scripts"/>
+
+    <script type="text/javascript" src="/xul/server/main/JSAN.js"/>
+    <script type="text/javascript" src="widget_prompt.js"/>
+
+    <vbox id="widget_prompt_topbar">
+        <description id="desc"/>
+    </vbox>
+    <vbox id="widget_prompt_main" flex="1" style="overflow: auto"/>
+    <vbox id="widget_prompt_bottombar">
+        <hbox id="widget_prompt_buttonbar">
+            <spacer flex="1"/>
+            <button id="ok_btn" />
+            <button id="cancel_btn" />
+        </hbox>
+    </vbox>
+
+</window>
+
index 7de93f9..14a169a 100644 (file)
@@ -1,6 +1,10 @@
 common.exception=!! This software has encountered an error.  Please tell your friendly system administrator or software developer the following:\n%1$s\n%2$s\n
 common.jsan.missing=The JSAN library object is missing.
 common.ok=Ok
+common.ok.label=Ok
+common.ok.accesskey=O
+common.cancel.label=Cancel
+common.cancel.accesskey=C
 common.clear=Clear
 common.confirm=Check here to confirm this message.
 common.error.default=Please report that this happened.
@@ -35,6 +39,9 @@ cat.opac.record_deleted.error=Error deleting record #%1$s : %2$s : %3$s
 cat.opac.record_undeleted.error=Error undeleting record #%1$s : %2$s : %3$s
 cat.opac.record_marked_for_overlay.record_id.alert=Record with ID %1$s marked for overlay.
 cat.opac.record_marked_for_overlay.tcn.alert=Record with TCN %1$s marked for overlay.
+cat.opac.record_marked_for_multi_home.record_id.alert=Record with ID %1$s targeted for Multi-Bib items.
+cat.opac.record_marked_for_multi_home.tcn.alert=Record with TCN %1$s targeted for Mutli-Bib items.
+cat.opac.clear_statusbar=Un-target/un-mark this record?
 cat.save_record=Save Record
 cat.save.failure=Record not likely updated.
 cat.record.counter=Record %1$s of %2$s
@@ -256,7 +263,8 @@ staff.cat.util.copy_editor.edit=Edit
 staff.cat.util.copy_editor.view=View
 staff.circ.copy_status.add_volumes.perm_failure=You do not have permission to add volumes to the workstation library.
 staff.circ.copy_status.add_volumes.title=Add Volume/Item for Record # %1$s
-staff.cat.manage_parts.title=Manage Parts for Record # %1$s
+staff.cat.copy_browser.marked_record_for_multi_home_indicator.tcn.label=Record with TCN %1$s targeted for Multi-Bib items.
+staff.cat.copy_browser.marked_record_for_multi_home_indicator.record_id.label=Record with ID %1$s targeted for Multi-Bib items.
 staff.cat.z3950.marked_record_for_overlay_indicator.tcn.label=Record with TCN %1$s marked for overlay.
 staff.cat.z3950.marked_record_for_overlay_indicator.record_id.label=Record with ID %1$s marked for overlay.
 staff.cat.opac.marked_record_for_hold_transfer_indicator.tcn.label=Record with TCN %1$s marked for title hold transfer.
index e962dfd..4e7fcf7 100644 (file)
@@ -871,6 +871,45 @@ cat.copy_browser.prototype = {
                                 obj.refresh_list();
                             }
                         ],
+
+                        'cmd_link_as_multi_bib' : [
+                            ['command'],
+                            function() {
+                                try {
+                                    obj.data.stash_retrieve();
+                                    if (!obj.data.marked_multi_home_record) {
+                                        alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.link_as_multi_bib.missing_bib'));
+                                        return;
+                                    }
+
+                                    JSAN.use('util.functional');
+
+                                    var list = util.functional.filter_list(
+                                        obj.sel_list,
+                                        function (o) {
+                                            return o.split(/_/)[0] == 'acp';
+                                        }
+                                    );
+
+                                    list = util.functional.map_list(
+                                        list,
+                                        function (o) {
+                                            return obj.map_acp[ o ].barcode();
+                                        }
+                                    );
+
+                                    xulG.new_tab(
+                                        window.xulG.url_prefix(urls.MANAGE_MULTI_HOME_ITEMS),
+                                        {},
+                                        { 'docid' : obj.data.marked_multi_home_record, 'barcodes' : list }
+                                    );
+
+                                } catch(E) {
+                                    alert('Error in copy_browser.js, cmd_link_as_multi_bib: ' + E);
+                                }
+                                obj.refresh_list();
+                            }
+                        ]
                     }
                 }
             );
index 319ae35..240fceb 100644 (file)
@@ -88,6 +88,7 @@ vim:noet:sw=4:ts=4:
         <command id="cmd_edit_items"/>
         <command id="cmd_delete_items"/>
         <command id="cmd_transfer_items"/>
+        <command id="cmd_link_as_multi_bib"/>
         <command id="cmd_print_spine_labels"/>
         <command id="cmd_add_volumes"/>
         <command id="cmd_edit_volumes"/>
@@ -111,6 +112,7 @@ vim:noet:sw=4:ts=4:
             <menuseparator/>
             <menuitem command="cmd_edit_items" label="&staff.cat.copy_browser.actions.cmd_edit_items.label;" accesskey="&staff.cat.copy_browser.actions.cmd_edit_items.accesskey;"/>
             <menuitem command="cmd_transfer_items" label="&staff.cat.copy_browser.actions.cmd_transfer_items.label;" accesskey="&staff.cat.copy_browser.actions.cmd_transfer_items.accesskey;"/>
+            <menuitem command="cmd_link_as_multi_bib" label="&staff.cat.copy_browser.actions.cmd_link_as_multi_bib.label;" accesskey="&staff.cat.copy_browser.actions.cmd_link_as_multi_bib.accesskey;"/>
             <menuseparator/>
             <menuitem command="cmd_add_volumes" label="&staff.cat.copy_browser.actions.cmd_add_volumes.label;" accesskey="&staff.cat.copy_browser.actions.cmd_add_volumes.accesskey;"/>
             <menuitem command="cmd_mark_library" label="&staff.cat.copy_browser.actions.cmd_mark_library.label;" accesskey="&staff.cat.copy_browser.actions.cmd_mark_library.accesskey;"/>
@@ -164,6 +166,7 @@ vim:noet:sw=4:ts=4:
                         <menuseparator/>
                         <menuitem command="cmd_edit_items" label="&staff.cat.copy_browser.holdings_maintenance.cmd_edit_items.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.cmd_edit_items.accesskey;"/>
                         <menuitem command="cmd_transfer_items" label="&staff.cat.copy_browser.holdings_maintenance.cmd_transfer_items.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.cmd_transfer_items.accesskey;"/>
+                        <menuitem command="cmd_link_as_multi_bib" label="&staff.cat.copy_browser.actions.cmd_link_as_multi_bib.label;" accesskey="&staff.cat.copy_browser.actions.cmd_link_as_multi_bib.accesskey;"/>
                         <menuseparator/>
                         <menuitem command="cmd_add_volumes" label="&staff.cat.copy_browser.holdings_maintenance.cmd_add_volumes.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.cmd_add_volumes.accesskey;"/>
                         <menuitem command="cmd_mark_library" label="&staff.cat.copy_browser.holdings_maintenance.cmd_mark_library.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.cmd_mark_library.accesskey;"/>
diff --git a/Open-ILS/xul/staff_client/server/cat/manage_multi_home_items.js b/Open-ILS/xul/staff_client/server/cat/manage_multi_home_items.js
new file mode 100644 (file)
index 0000000..518b665
--- /dev/null
@@ -0,0 +1,477 @@
+var data; var list; var error; var net; var sound;
+var rows = {};
+var bpbcm_barcode_map = {};
+
+var commonStrings;
+var catStrings;
+
+//// parent interfaces may call this
+function default_focus() { document.getElementById('scanbox').focus(); }
+////
+
+function my_init() {
+    try {
+        netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); 
+
+        commonStrings = $('commonStrings');
+        catStrings = $('catStrings');
+
+        if (typeof JSAN == 'undefined') {
+            throw(
+                commonStrings.getString('common.jsan.missing')
+            );
+        }
+
+        JSAN.errorLevel = "die"; // none, warn, or die
+        JSAN.addRepository('..');
+
+        JSAN.use('util.error'); error = new util.error();
+        JSAN.use('util.sound'); sound = new util.sound();
+        JSAN.use('util.widgets');
+        JSAN.use('util.functional');
+        JSAN.use('util.list');
+        JSAN.use('OpenILS.data'); data = new OpenILS.data();
+        data.stash_retrieve();
+        JSAN.use('util.network'); net = new util.network();
+        dojo.require('openils.PermaCrud');
+        JSAN.use('cat.util');
+
+        init_menu();
+        init_list();
+        $('list_actions').appendChild( list.render_list_actions() );
+        list.set_list_actions();
+        populate_list();
+        $('submit').addEventListener('command', function() { handle_submit(true); }, false);
+        $('remove').addEventListener('command', function() { handle_remove(); }, false);
+        $('change').addEventListener('command', function() { handle_change(); }, false);
+        $('opac').addEventListener('command', function() { handle_opac(); }, false);
+        $('scanbox').addEventListener('keypress', handle_keypress, false);
+        default_focus();
+
+        if (typeof xulG.set_tab_name == 'function') {
+            xulG.set_tab_name(
+                catStrings.getFormattedString(
+                    'staff.cat.manage_multi_bib_items.tab_name',
+                    [ xul_param('docid') ]
+                )
+            );
+        }
+
+        if (! xul_param('no_bib_summary')) {
+            if (typeof bib_brief_overlay == 'function') {
+                $("bib_brief_groupbox").hidden = false;
+                bib_brief_overlay( { 'mvr_id' : xul_param('docid') } );
+            }
+        }
+
+    } catch(E) {
+        alert('Error in manage_multi_home_items.js, my_init(): ' + E);
+    }
+}
+
+function init_menu() {
+    try {
+        var ml = util.widgets.make_menulist(
+            util.functional.map_list(
+                data.list.bpt.sort( function(a,b) {
+                    if (a.name().toUpperCase() < b.name().toUpperCase()) return -1;
+                    if (a.name().toUpperCase() > b.name().toUpperCase()) return 1;
+                    return 0;
+                }),
+                function(obj) {
+                    return [ obj.name(), obj.id() ];
+                }
+            )
+        );
+        ml.setAttribute('id','bpt_menu');
+        $('menu_placeholder').appendChild(ml);
+    } catch(E) {
+        alert('Error in manage_multi_home_items.js, init_menu(): ' + E);
+    }
+}
+
+function init_list() {
+    try {
+        list = new util.list( 'list' );
+        list.init( 
+            {
+                'retrieve_row' : function(params) {
+                    if (params.row.my.bpbcm) {
+                        params.row_node.setAttribute('retrieve_id',params.row.my.bpbcm.id());
+                    }
+                    params.on_retrieve(params.row);
+                    return params.row;
+                },
+                'columns' : [
+                    {
+                        'id' : 'result',
+                        'label' : 'Result',
+                        'flex' : 1,
+                        'primary' : false,
+                        'hidden' : false,
+                        'editable' : false, 'render' : function(my) { return my.result; }
+                    }
+                ].concat(
+                    list.fm_columns('acp', {
+                        '*' : { 'expanded_label' : false, 'hidden' : true },
+                        'acp_barcode' : { 'hidden' : false },
+                        'acp_opac_visible' : { 'hidden' : false },
+                        'acp_holdable' : { 'hidden' : false }
+                    })
+                ).concat(
+                    list.fm_columns('mvr', {
+                        '*' : { 'expanded_label' : false, 'hidden' : true }, 
+                        'mvr_title' : { 'hidden' : false },
+                        'mvr_author' : { 'hidden' : false },
+                        'mvr_isbn' : { 'hidden' : false },
+                        'mvr_tcn' : { 'hidden' : false },
+                        'mvr_id' : { 'hidden' : false }
+                    })
+                ).concat(
+                    list.fm_columns('bpbcm', {
+                        '*' : { 'expanded_label' : false, 'hidden' : true },
+                        'bpbcm_peer_type' : {
+                            'hidden' : false,
+                            'render' : function(my) { return my.bpbcm ? data.hash.bpt[ my.bpbcm.peer_type() ].name() : ''; }
+                        }
+                    })
+                )
+            }
+        );
+    } catch(E) {
+        alert('Error in manage_multi_home_items.js, init_list(): ' + E);
+    }
+}
+
+function handle_keypress(ev) {
+    try {
+        if (ev.keyCode && ev.keyCode == 13) {
+            handle_submit(true);
+        }
+    } catch(E) {
+        alert('Error in manage_multi_home_items.js, handle_keypress(): ' + E);
+    }
+}
+
+function handle_submit(create,my_bpbcm,my_barcode) {
+    try {
+        var barcode;
+        if (create) {
+            if (my_barcode) {
+                barcode = my_barcode;
+            } else {
+                barcode = $('scanbox').value;
+                $('scanbox').value = '';
+                default_focus();
+            }
+        }
+
+        var placeholder_acp = new acp();
+        placeholder_acp.barcode(barcode);
+        var row_params = {
+            'row' : {
+                'my' : {
+                    'acp' : placeholder_acp,
+                    'bpbcm' : my_bpbcm
+                }
+            }
+        };
+
+        if (barcode && rows[barcode]) {
+                var node = rows[barcode].my_node;
+                var parentNode = node.parentNode;
+                parentNode.removeChild( node );
+                delete(rows[barcode]);
+        }
+
+        row_params = list.append(row_params);
+        if (barcode) {
+            rows[barcode] = row_params;
+        }
+
+        function handle_req(req) {
+            try {
+                var robj = req.getResultObject();
+                row_params.row.my.result = catStrings.getString('staff.cat.manage_multi_bib_items.result.column.value.error');
+                if (typeof robj.ilsevent != 'undefined') {
+                    row_params.row.my.result = robj.textcode;
+                } else {
+                    rows[robj.copy.barcode()] = row_params;
+                    if (row_params.row.my.bpbcm) {
+                        bpbcm_barcode_map[ row_params.row.my.bpbcm.id() ] = robj.copy.barcode();
+                    }
+
+                    row_params.row.my.acp = robj.copy;
+                    row_params.row.my.mvr = robj.mvr;
+
+                    if (create && robj.mvr.doc_id() != xul_param('docid')) {
+                        var new_bpbcm = new bpbcm();
+                            new_bpbcm.isnew(1);
+                            new_bpbcm.peer_type($('bpt_menu').value);
+                            new_bpbcm.peer_record(xul_param('docid'));
+                            new_bpbcm.target_copy(robj.copy.id());
+                        var pcrud = new openils.PermaCrud( { authtoken :ses() });
+                        pcrud.create(new_bpbcm, {
+                            "onerror" : function(r) {
+                                dump('onerror, r = ' + js2JSON(r) + '\n');
+                            },
+                            "oncomplete": function (r, objs) {
+                                try {
+                                    var obj = objs[0];
+                                    if (obj) {
+                                        row_params.row.my.result = catStrings.getString('staff.cat.manage_multi_bib_items.result.column.value.success');
+                                        row_params.row.my.bpbcm = obj;
+                                        bpbcm_barcode_map[ obj.id() ] = robj.copy.barcode();
+                                    } else {
+                                        row_params.row.my.result = catStrings.getString('staff.cat.manage_multi_bib_items.result.column.value.failed');
+                                        sound.bad();
+                                    }
+                                    list.refresh_row( row_params );
+                                } catch(E) {
+                                    alert('Error in manage_multi_home_items.js, handle_submit, pcrud create oncomplete callback: ' + E);
+                                }
+                            }
+                        });
+                    } else {
+                        if (robj.mvr.doc_id() != xul_param('docid')) {
+                            row_params.row.my.result = catStrings.getString('staff.cat.manage_multi_bib_items.result.column.value.item_linked_to_bib');
+                        } else {
+                            row_params.row.my.result = catStrings.getString('staff.cat.manage_multi_bib_items.result.column.value.item_native_to_bib');
+                        }
+                    }
+                }
+                list.refresh_row( row_params );
+            } catch(E) {
+                alert('Error in manage_multi_home_items.js, handle_submit, acp details callback: ' + E);
+            }
+        }
+
+        if (my_bpbcm) {
+            net.simple_request(
+                'FM_ACP_DETAILS', // FIXME: want this to be authoritative
+                [ ses(), my_bpbcm.target_copy() ],
+                handle_req
+            );
+        } else {
+            net.simple_request(
+                'FM_ACP_DETAILS_VIA_BARCODE.authoritative',
+                [ ses(), barcode ],
+                handle_req
+            );
+        }
+
+    } catch(E) {
+        alert('Error in manage_multi_home_items.js, handle_submit(): ' + E);
+    }
+}
+
+function populate_list() {
+    try {
+        var pcrud = new openils.PermaCrud( { authtoken :ses() });
+        pcrud.search(
+            'bpbcm',
+            {
+                peer_record : xul_param('docid')
+            },
+            {
+                async : true,
+                streaming : true,
+                onerror : function(r) {
+                        alert('Error in manage_multi_home_items.js, populate_list(), pcrud.search onerror: ' + r);
+                },
+                oncomplete : function() {
+                    if (xul_param('barcodes')) { // incoming from Holdings Maintenance
+                        handle_barcodes( xul_param('barcodes') );
+                    }
+                },
+                onresponse : function(r) {
+                    try {
+                        var my_bpbcm = openils.Util.readResponse(r);
+                        if (typeof my_bpbcm.ils_event != 'undefined') { throw(my_bpbcm); }
+                        handle_submit(false,my_bpbcm);
+                    } catch(E) {
+                        alert('Error in manage_multi_home_items.js, populate_list(), pcrud.search onresponse: ' + E);
+                    }
+                }
+            }
+        );
+
+    } catch(E) {
+        alert('Error in manage_multi_home_items.js, populate_list(): ' + E);
+    }
+}
+
+function handle_change() {
+    try {
+        var node_list = list.retrieve_selection();
+        var eligibles = [];
+        for (var i = 0; i < node_list.length; i++) {
+            var retrieve_id = node_list[i].getAttribute('retrieve_id');
+            if (retrieve_id && retrieve_id != 'undefined') {
+                eligibles.push( retrieve_id );
+            }
+        }
+        if (eligibles.length > 0) {
+            var new_peer_type = widget_prompt( $('bpt_menu').cloneNode(true), {
+                'title' : catStrings.getString('staff.cat.manage_multi_bib_items.prompt.title')
+            });
+
+            if (new_peer_type) {
+                var bpbcm_list = [];
+                for (var i = 0; i < eligibles.length; i++) {
+                    var obj = rows[ bpbcm_barcode_map[ eligibles[i] ] ].row.my.bpbcm;
+                    obj.ischanged(1);
+                    obj.peer_type( new_peer_type );
+                    bpbcm_list.push( obj );
+                }
+                var pcrud = new openils.PermaCrud( { authtoken :ses() });
+                pcrud.update(
+                    bpbcm_list, {
+                        'async' : false,
+                        'onerror': function(r) {
+                            dump('onerror: ' + r + '\n');
+                        },
+                        'onresponse': function(r) {
+                            dump('onresponse: ' + r + '\n');
+                        },
+                        'oncomplete': function(r,ids) {
+                            dump('oncomplete: r = ' + r + '\n\tids = ' + js2JSON(ids) + '\n');
+                            for (var i = 0; i < ids.length; i++) {
+                                var bpbcm_id = ids[i];
+                                try {
+                                    rows[ bpbcm_barcode_map[ bpbcm_id ] ].row.my.bpbcm.peer_type( new_peer_type );
+                                    rows[ bpbcm_barcode_map[ bpbcm_id ] ].row.my.result = catStrings.getString('staff.cat.manage_multi_bib_items.result.column.value.peer_type_updated');
+                                    list.refresh_row( rows[ bpbcm_barcode_map[ bpbcm_id ] ] );
+                                } catch(E) {
+                                    alert('error in oncomplete: ' + E);
+                                }
+                            }
+                        }
+                    }
+                );
+            }
+        }
+
+    } catch(E) {
+        alert('Error in manage_multi_home_items.js, handle_change(): ' + E);
+    }
+}
+
+function handle_remove() {
+    try {
+        var node_list = list.retrieve_selection();
+        var eligibles = [];
+        for (var i = 0; i < node_list.length; i++) {
+            var retrieve_id = node_list[i].getAttribute('retrieve_id');
+            if (retrieve_id && retrieve_id != 'undefined') {
+                eligibles.push( retrieve_id );
+            }
+        }
+        if (eligibles.length > 0) {
+            if (window.confirm(
+                eligibles.length == 1
+                ? catStrings.getFormattedString(
+                        'staff.cat.manage_multi_bib_items.prompt.confirm.unlink_item_from_bib.singular',
+                        [ xul_param('docid') ]
+                )
+                : catStrings.getFormattedString(
+                        'staff.cat.manage_multi_bib_items.prompt.confirm.unlink_item_from_bib.plural',
+                        [ xul_param('docid'), eligibles.length ]
+                ))
+            ) {
+                var bpbcm_list = [];
+                for (var i = 0; i < eligibles.length; i++) {
+                    var obj = rows[ bpbcm_barcode_map[ eligibles[i] ] ].row.my.bpbcm;
+                    obj.isdeleted(1);
+                    bpbcm_list.push( obj );
+                }
+                var pcrud = new openils.PermaCrud( { authtoken :ses() });
+                pcrud.eliminate(
+                    bpbcm_list, {
+                        'async' : false,
+                        'onerror': function(r) {
+                            dump('onerror: ' + r + '\n');
+                        },
+                        'onresponse': function(r) {
+                            dump('onresponse: ' + r + '\n');
+                        },
+                        'oncomplete': function(r,ids) {
+                            dump('oncomplete: r = ' + r + '\n\tids = ' + js2JSON(ids) + '\n');
+                            for (var i = 0; i < ids.length; i++) {
+                                var bpbcm_id = ids[i];
+                                try {
+                                    var node = rows[ bpbcm_barcode_map[ bpbcm_id ] ].my_node;
+                                    var parentNode = node.parentNode;
+                                    parentNode.removeChild( node );
+                                    delete(rows[ bpbcm_barcode_map[ bpbcm_id ] ]);
+                                } catch(E) {
+                                    alert('error in oncomplete: ' + E);
+                                }
+                            }
+                        }
+                    }
+                );
+            }
+        }
+
+    } catch(E) {
+        alert('Error in manage_multi_home_items.js, handle_remove(): ' + E);
+    }
+}
+
+function handle_opac() {
+    try {
+        var node_list = list.retrieve_selection();
+        var eligibles = [];
+        for (var i = 0; i < node_list.length; i++) {
+            var retrieve_id = node_list[i].getAttribute('retrieve_id');
+            if (retrieve_id && retrieve_id != 'undefined') {
+                eligibles.push( retrieve_id );
+            }
+        }
+        if (eligibles.length > 0) {
+            var selection_list = [];
+            for (var i = 0; i < eligibles.length; i++) {
+                selection_list.push({
+                    'barcode' : bpbcm_barcode_map[ eligibles[i] ]
+                });
+            }
+            cat.util.show_in_opac(selection_list);
+        }
+
+    } catch(E) {
+        alert('Error in manage_multi_home_items.js, handle_opac(): ' + E);
+    }
+}
+
+function handle_barcodes(barcodes) {
+    try {
+        var funcs = [];
+
+        for (var i = 0; i < barcodes.length; i++) {
+            if (typeof rows[barcodes[i]] == 'undefined') {
+                funcs.push(
+                    function(barcode) {
+
+                        return function() {
+                            handle_submit(true,null,barcode);
+                        };
+
+                    }(barcodes[i])
+                )
+            }
+        }
+
+        JSAN.use('util.exec'); var exec = new util.exec();
+        exec.timer( funcs, 500 );
+
+        funcs.push(
+            function() {
+                exec.clear_timer();
+            }
+        );
+
+    } catch(E) {
+        alert('Error in manage_multi_home_items.js, handle_barcodes(): ' + E);
+    }
+}
diff --git a/Open-ILS/xul/staff_client/server/cat/manage_multi_home_items.xul b/Open-ILS/xul/staff_client/server/cat/manage_multi_home_items.xul
new file mode 100644 (file)
index 0000000..cb905a7
--- /dev/null
@@ -0,0 +1,92 @@
+<?xml version="1.0"?>
+<!-- Application: Evergreen Staff Client -->
+<!-- Screen: Add Multi-Home Items to specific Bib -->
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- STYLESHEETS -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/global.css" type="text/css"?>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- LOCALIZATION -->
+<!DOCTYPE window PUBLIC "" ""[
+    <!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- OVERLAYS -->
+<?xul-overlay href="/xul/server/OpenILS/util_overlay.xul"?>
+<?xul-overlay href="/xul/server/cat/bib_brief_overlay.xul"?>
+
+<window id="main_win" 
+    onload="try { my_init(); font_helper(); persist_helper(); } catch(E) { alert(E); }"
+    xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+    <!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+    <!-- BEHAVIOR -->
+    <script type="text/javascript">
+        var myPackageDir = 'open_ils_staff_client'; var IAMXUL = true;
+    </script>
+    <scripts id="openils_util_scripts"/>
+
+    <messagecatalog id="catStrings" src="/xul/server/locale/<!--#echo var='locale'-->/cat.properties" />
+    <messagecatalog id="circStrings" src="/xul/server/locale/<!--#echo var='locale'-->/circ.properties" />
+
+    <script type="text/javascript" src="/xul/server/main/JSAN.js"/>
+    <script type="text/javascript" src="manage_multi_home_items.js"/>
+
+    <commandset>
+        <command id="opac"
+            label="&staff.cat.manage_multi_bib_items.actions.menu_entry.show_in_opac.label;"
+            accesskey="&staff.cat.manage_multi_bib_items.actions.menu_entry.show_in_opac.accesskey;"/>
+        <command id="change"
+            label="&staff.cat.manage_multi_bib_items.actions.menu_entry.change_peer_type.label;"
+            accesskey="&staff.cat.manage_multi_bib_items.actions.menu_entry.change_peer_type.accesskey;"/>
+        <command id="remove"
+            label="&staff.cat.manage_multi_bib_items.actions.menu_entry.unlink_from_bib.label;"
+            accesskey="&staff.cat.manage_multi_bib_items.actions.menu_entry.unlink_from_bib.accesskey;"/>
+    </commandset>
+
+    <popupset>
+        <popup id="actions">
+            <menuitem command="opac" />
+            <menuitem command="change" />
+            <menuitem command="remove" />
+        </popup>
+    </popupset>
+
+    <groupbox id="bib_brief_groupbox" hidden="true">
+        <caption label="&staff.cat.bib_brief.record_summary;"/>
+        <grid id="bib_brief_grid"/>
+    </groupbox>
+    <groupbox flex="1" class="my_overflow">
+        <caption label="&staff.cat.manage_multi_bib_items.caption;"/>
+        <hbox>
+            <label control="bpt_menu"
+                value="&staff.cat.manage_multi_bib_items.peer_type.menu.label;"
+                accesskey="&staff.cat.manage_multi_bib_items.peer_type.menu.accesskey;"/>
+            <hbox id="menu_placeholder" />
+            <label control="scanbox"
+                value="&staff.cat.manage_multi_bib_items.barcode.textbox.label;"
+                accesskey="&staff.cat.manage_multi_bib_items.barcode.textbox.accesskey;"/>
+            <textbox id="scanbox"/>
+            <button id="submit"
+                label="&staff.cat.manage_multi_bib_items.barcode.submit.label;"
+                accesskey="&staff.cat.manage_multi_bib_items.barcode.submit.accesskey;"/>
+            <spacer flex="1"/>
+            <menubar>
+                <menu label="&staff.cat.manage_multi_bib_items.actions.menu.label;" accesskey="&staff.cat.manage_multi_bib_items.actions.menu.accesskey;">
+                    <menupopup>
+                        <menuitem command="opac" />
+                        <menuitem command="change" />
+                        <menuitem command="remove" />
+                    </menupopup>
+                </menu>
+            </menubar>
+        </hbox>
+        <tree id="list" flex="1" enableColumnDrag="true" context="actions"/>
+        <hbox id="list_actions" />
+    </groupbox>
+
+</window>
+
index b71cdb5..51e080f 100644 (file)
@@ -9,6 +9,19 @@ staff.cat.bib_brief.deleted=(Deleted)
 staff.cat.bib_brief.inactive=(Inactive)
 staff.cat.bib_brief.noncat=(Not Cataloged)
 staff.cat.bib_brief.noncat.alert=Item not cataloged.
+# %1$s = Bib Record ID
+staff.cat.manage_multi_bib_items.tab_name=Manage Foreign Items for Bib %1$s
+staff.cat.manage_multi_bib_items.result.column.value.error=Error
+staff.cat.manage_multi_bib_items.result.column.value.success=Success
+staff.cat.manage_multi_bib_items.result.column.value.failed=Failed
+staff.cat.manage_multi_bib_items.result.column.value.item_linked_to_bib=Item linked to bib
+staff.cat.manage_multi_bib_items.result.column.value.item_native_to_bib=Item native to bib
+staff.cat.manage_multi_bib_items.result.column.value.peer_type_updated=Peer Type updated
+staff.cat.manage_multi_bib_items.prompt.title=Change Peer Type
+# %1$s = Bib Record ID
+staff.cat.manage_multi_bib_items.prompt.confirm.unlink_item_from_bib.singular=Unlink selected item from Bib %1$s
+# %1$s = Bib Record ID, %2$s = Number of selected items
+staff.cat.manage_multi_bib_items.prompt.confirm.unlink_item_from_bib.plural=Unlink %2$s selected items from Bib %1$s
 staff.cat.copy_browser.add_item.title=Add Item
 staff.cat.copy_browser.add_item.error=copy browser -> add copies
 staff.cat.copy_browser.add_items_bucket.error=copy browser -> add copies to bucket
@@ -64,6 +77,7 @@ staff.cat.copy_browser.transfer.success=Volumes transferred.
 staff.cat.copy_browser.transfer.unexpected_error=All volumes not likely transferred.
 staff.cat.copy_browser.transfer_items.missing_volume=Please mark a volume as the destination from within holdings maintenance and then try this again.
 staff.cat.copy_browser.transfer_items.unexpected_error=All copies not likely transferred.
+staff.cat.copy_browser.link_as_multi_bib.missing_bib=Please Mark a bib record as a Target for Foreign Items and try this again.
 staff.cat.copy_browser.missing_library=Missing library list.
 staff.cat.copy_browser.consortial_copy_count.error=Error retrieving consortial copy count.
 staff.cat.copy_browser.list_init.tree_location=Location/Barcode