Merge branch 'master' of git.evergreen-ils.org:Evergreen into social
authorMike Rylander <mrylander@gmail.com>
Mon, 16 May 2011 20:35:54 +0000 (16:35 -0400)
committerMike Rylander <mrylander@gmail.com>
Mon, 16 May 2011 20:35:54 +0000 (16:35 -0400)
122 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/apachemods/mod_idlchunk.c
Open-ILS/src/extras/Makefile.install
Open-ILS/src/perlmods/lib/OpenILS/Application/Cat.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Serial.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Serial.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/actor.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/biblio.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Vandelay.pm
Open-ILS/src/perlmods/lib/OpenILS/SIP/Patron.pm
Open-ILS/src/perlmods/lib/OpenILS/Utils/MFHD/Caption.pm
Open-ILS/src/perlmods/lib/OpenILS/Utils/MFHD/Date.pm
Open-ILS/src/perlmods/lib/OpenILS/Utils/MFHD/Holding.pm
Open-ILS/src/perlmods/lib/OpenILS/Utils/MFHD/test/mfhd.t
Open-ILS/src/perlmods/lib/OpenILS/Utils/MFHD/test/mfhddata.txt
Open-ILS/src/perlmods/lib/OpenILS/Utils/MFHDParser.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/AddedContent/ContentCafe.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/AddedContent/OpenLibrary.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
Open-ILS/src/reporter/clark-kent.pl
Open-ILS/src/sql/Pg/002.functions.config.sql
Open-ILS/src/sql/Pg/002.schema.config.sql
Open-ILS/src/sql/Pg/005.schema.actors.sql
Open-ILS/src/sql/Pg/011.schema.authority.sql
Open-ILS/src/sql/Pg/012.schema.vandelay.sql
Open-ILS/src/sql/Pg/020.schema.functions.sql
Open-ILS/src/sql/Pg/030.schema.metabib.sql
Open-ILS/src/sql/Pg/040.schema.asset.sql
Open-ILS/src/sql/Pg/099.matrix_weights.sql
Open-ILS/src/sql/Pg/1.6.1-2.0-upgrade-db.sql
Open-ILS/src/sql/Pg/100.circ_matrix.sql
Open-ILS/src/sql/Pg/110.hold_matrix.sql
Open-ILS/src/sql/Pg/2.0-2.1-upgrade-db.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/999.functions.global.sql
Open-ILS/src/sql/Pg/build-db.sh
Open-ILS/src/sql/Pg/make-db-patch.pl [new file with mode: 0755]
Open-ILS/src/sql/Pg/upgrade/0524.data.toggle_unified_volume_copy_editor.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0525.schema.phys-char-regression.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0526.schema.upgrade-dep-tracking.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0527.schema.matrix-bib_level.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0528.schema.functions_assume_unicode.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0529.data.merge_user-ou_settings.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0530.schema.actor-usr-index-phone-fields-more.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0531.schema.auditor_affixes.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0532.schema.fix_copy_count_funcs.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0533.schema.fix_age_circ.sql [new file with mode: 0644]
Open-ILS/src/support-scripts/authority_control_fields.pl.in [moved from Open-ILS/src/support-scripts/authority_control_fields.pl with 76% similarity]
Open-ILS/src/support-scripts/marc_stream_importer.pl
Open-ILS/web/css/skin/default/acq.css
Open-ILS/web/js/dojo/openils/acq/Lineitem.js
Open-ILS/web/js/dojo/openils/actor/nls/register.js
Open-ILS/web/js/dojo/openils/widget/nls/Searcher.js
Open-ILS/web/js/ui/default/acq/invoice/view.js
Open-ILS/web/js/ui/default/actor/user/register.js
Open-ILS/web/opac/locale/en-US/lang.dtd
Open-ILS/web/opac/skin/default/css/layout.css
Open-ILS/web/opac/skin/default/js/adv_global.js
Open-ILS/web/opac/skin/default/js/cn_browse.js
Open-ILS/web/opac/skin/default/js/copy_details.js
Open-ILS/web/opac/skin/default/js/myopac.js
Open-ILS/web/opac/skin/default/js/rdetail.js
Open-ILS/web/opac/skin/default/xml/common/js_common.xml
Open-ILS/web/templates/default/acq/invoice/view.tt2
Open-ILS/web/templates/default/actor/user/register.tt2
Open-ILS/web/templates/default/actor/user/register_table.tt2
Open-ILS/web/templates/default/conify/global/config/circ_matrix_matchpoint.tt2
Open-ILS/web/templates/default/conify/global/config/hold_matrix_matchpoint.tt2
Open-ILS/xul/staff_client/Makefile.am
Open-ILS/xul/staff_client/chrome/content/OpenILS/global_util.js
Open-ILS/xul/staff_client/chrome/content/OpenILS/util_overlay_chrome.xul
Open-ILS/xul/staff_client/chrome/content/OpenILS/util_overlay_offline.xul
Open-ILS/xul/staff_client/chrome/content/cat/opac.js
Open-ILS/xul/staff_client/chrome/content/main/menu.js
Open-ILS/xul/staff_client/chrome/content/main/simple_auth.xul
Open-ILS/xul/staff_client/chrome/content/util/print.js
Open-ILS/xul/staff_client/external/prune_dirs.sh
Open-ILS/xul/staff_client/server/OpenILS/symbol_overlay.js [new file with mode: 0644]
Open-ILS/xul/staff_client/server/OpenILS/symbol_overlay.xul [new file with mode: 0644]
Open-ILS/xul/staff_client/server/OpenILS/util_overlay.xul
Open-ILS/xul/staff_client/server/admin/printer_settings.js
Open-ILS/xul/staff_client/server/admin/stat_cat_editor.js
Open-ILS/xul/staff_client/server/cat/bib_brief.js
Open-ILS/xul/staff_client/server/cat/copy_browser.js
Open-ILS/xul/staff_client/server/cat/copy_summary.xul
Open-ILS/xul/staff_client/server/cat/marcedit.xul
Open-ILS/xul/staff_client/server/cat/volume_copy_creator.js
Open-ILS/xul/staff_client/server/circ/checkin.js
Open-ILS/xul/staff_client/server/circ/checkin_overlay.xul
Open-ILS/xul/staff_client/server/circ/checkout.js
Open-ILS/xul/staff_client/server/circ/copy_status.js
Open-ILS/xul/staff_client/server/circ/pre_cat_fields.xul
Open-ILS/xul/staff_client/server/circ/util.js
Open-ILS/xul/staff_client/server/index.xhtml
Open-ILS/xul/staff_client/server/locale/en-US/serial.properties
Open-ILS/xul/staff_client/server/patron/bill_details.js
Open-ILS/xul/staff_client/server/patron/display.js
Open-ILS/xul/staff_client/server/patron/holds.js
Open-ILS/xul/staff_client/server/patron/summary_overlay.xul
Open-ILS/xul/staff_client/server/patron/summary_overlay_horiz.xul
Open-ILS/xul/staff_client/server/serial/manage_items.js
Open-ILS/xul/staff_client/server/serial/manage_items.xul
Open-ILS/xul/staff_client/server/serial/sbsum_editor.js
Open-ILS/xul/staff_client/server/serial/scap_editor.js
Open-ILS/xul/staff_client/server/serial/sdist_editor.js
Open-ILS/xul/staff_client/server/serial/siss_editor.js
Open-ILS/xul/staff_client/server/serial/sisum_editor.js
Open-ILS/xul/staff_client/server/serial/sssum_editor.js
Open-ILS/xul/staff_client/server/skin/cat.css
Open-ILS/xul/staff_client/server/skin/custom.js.example
Open-ILS/xul/staff_client/server/skin/global.css
Open-ILS/xul/staff_client/windowssetup.nsi
README
build/i18n/po/register.js/register.js.pot
build/tools/update.sh
build/tools/update_git_svn.sh [deleted file]
configure.ac

index 150b0ec..23b131e 100644 (file)
@@ -1232,6 +1232,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Circulation Modifier" name="circ_modifier" oils_persist:primitive="string" reporter:datatype="link"/>
                        <field reporter:label="MARC Type" name="marc_type" oils_persist:primitive="string" reporter:datatype="link"/>
                        <field reporter:label="MARC Form" name="marc_form" oils_persist:primitive="string" reporter:datatype="link"/>
+                       <field reporter:label="MARC Bib Level" name="marc_bib_level" oils_persist:primitive="string" reporter:datatype="link"/>
                        <field reporter:label="Videorecording Format" name="marc_vr_format" oils_persist:primitive="string" reporter:datatype="link"/>
                        <field reporter:label="Reference?" name="ref_flag" reporter:datatype="bool"/>
                        <field reporter:label="Holdable?" name="holdable" reporter:datatype="bool"/>
@@ -1252,6 +1253,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <link field="circ_modifier" reltype="has_a" key="code" map="" class="ccm"/>
                        <link field="marc_type" reltype="has_a" key="code" map="" class="citm"/>
                        <link field="marc_form" reltype="has_a" key="code" map="" class="cifm"/>
+                       <link field="marc_bib_level" reltype="has_a" key="code" map="" class="cblvl"/>
                        <link field="marc_vr_format" reltype="has_a" key="code" map="" class="cvrfm"/>
                        <link field="age_hold_protect_rule" reltype="has_a" key="id" map="" class="crahp"/>
             <link field="transit_range" reltype="has_a" key="id" map="" class="aout"/>
@@ -1279,6 +1281,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Circulation Modifier" name="circ_modifier" oils_persist:primitive="string" reporter:datatype="link"/>
                        <field reporter:label="MARC Type" name="marc_type" oils_persist:primitive="string" reporter:datatype="link"/>
                        <field reporter:label="MARC Form" name="marc_form" oils_persist:primitive="string" reporter:datatype="link"/>
+                       <field reporter:label="MARC Bib Level" name="marc_bib_level" oils_persist:primitive="string" reporter:datatype="link"/>
                        <field reporter:label="Videorecording Format" name="marc_vr_format" oils_persist:primitive="string" reporter:datatype="link"/>
                        <field reporter:label="Reference?" name="ref_flag" reporter:datatype="bool"/>
             <field reporter:label="Juvenile?" name="juvenile_flag" reporter:datatype="bool"/>
@@ -1304,6 +1307,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <link field="circ_modifier" reltype="has_a" key="code" map="" class="ccm"/>
                        <link field="marc_type" reltype="has_a" key="code" map="" class="citm"/>
                        <link field="marc_form" reltype="has_a" key="code" map="" class="cifm"/>
+                       <link field="marc_bib_level" reltype="has_a" key="code" map="" class="cblvl"/>
                        <link field="marc_vr_format" reltype="has_a" key="code" map="" class="cvrfm"/>
                        <link field="duration_rule" reltype="has_a" key="id" map="" class="crcd"/>
                        <link field="max_fine_rule" reltype="has_a" key="id" map="" class="crmf"/>
@@ -3599,6 +3603,7 @@ SELECT  usr,
                <fields oils_persist:primary="id" oils_persist:sequence="serial.distribution_id_seq">
                        <field reporter:label="ID" name="id" reporter:datatype="id"/>
                        <field reporter:label="Legacy Record Entry" name="record_entry" reporter:datatype="link"/>
+                       <field reporter:label="Summary Method" name="summary_method" reporter:datatype="text"/>
                        <field reporter:label="Subscription" name="subscription" reporter:datatype="link"/>
                        <field reporter:label="Holding Lib" name="holding_lib" reporter:datatype="org_unit"/>
                        <field reporter:label="Label" name="label" reporter:datatype="text"/>
index 068a448..c0b815c 100644 (file)
@@ -497,7 +497,7 @@ static int idlChunkHandler( ap_filter_t *f, apr_bucket_brigade *brigade ) {
        idlChunkConfig* config = ap_get_module_config( 
                        f->r->per_dir_config, &idlchunk_module );
 
-       ap_log_rerror(APLOG_MARK, APLOG_ERR, 
+       ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 
                        0, f->r, "IDLCHUNK Config:\nContent Type = %s, "
                        "Strip PI = %s, Strip Comments = %s, Doctype = %s", 
                        config->contentType, 
index 9a34072..296b6e7 100644 (file)
@@ -4,17 +4,13 @@
 #
 # Makefile to install prerequisites for OpenSRF and Evergreen
 #
-# Currently supports Debian (lenny/squeeze), Ubuntu (hardy/lucid), and Fedora (13).
+# Currently supports Debian (squeeze), Ubuntu (lucid), and Fedora (14).
 # Working towards support of CentOS 5 / RHEL 5.
 # Installs Perl prereqs, libjs with Perl wrapper, libdbi, libdbi-drivers, and libyaz
 #
 # usage:
-#      make -f Makefile.install debian-lenny
-#      - or -
 #      make -f Makefile.install debian-squeeze
 #      - or -
-#      make -f Makefile.install ubuntu-hardy
-#      - or -
 #      make -f Makefile.install ubuntu-lucid
 #      - or -
 #      make -f Makefile.install fedora14
@@ -25,8 +21,8 @@
 #
 # Notes:
 #
-#      This makefile has been tested much more with Debian and Ubuntu than
-#      Fedora, CentOS, or RHEL.
+#      This makefile has been tested much more with Debian, Ubuntu, and
+#      Fedora than CentOS, or RHEL.
 #
 # ---------------------------------------------------------------------
  
@@ -155,7 +151,7 @@ CENTOS_PERL = \
        Net::XMPP \
        Net::Z3950::ZOOM
 
-FEDORA_13_RPMS = \
+FEDORA_RPMS = \
        aspell \
        aspell-en \
        libdbi \
@@ -183,7 +179,7 @@ FEDORA_13_RPMS = \
        perl-Text-CSV \
        perl-Text-CSV_XS \
        perl-XML-Writer \
-       postgresql-devel \
+       postgresql90-devel \
        readline-devel \
        tcp_wrappers-devel \
        wget \
@@ -192,33 +188,28 @@ FEDORA_13_RPMS = \
 # Note: B:O:AuthorizeNet 3.21 fails with https://rt.cpan.org/Public/Bug/Display.html?id=55172
 # Should be fixed in 3.22
 # MARC::Record 2.0.1 is required but only 2.0.0 is packaged
-FEDORA_13_CPAN = \
+FEDORA_CPAN = \
        Business::OnlinePayment \
        Business::OnlinePayment::AuthorizeNet \
        MARC::Record \
        UUID::Tiny
 
-PGSQL_84_RPMS = \
-       postgresql-8.4* \
-       postgresql-contrib-8.4* \
-       postgresql-devel-8.4* \
-       postgresql-plpe*-8.4* \
-       postgresql-server-8.4*
-
-PGSQL_CLIENT_DEBS_82 = \
-       postgresql-client
+PGSQL_90_RPMS = \
+       postgresql90 \
+       postgresql90-contrib \
+       postgresql90-devel \
+       postgresql90-libs \
+       postgresql90-plperl \
+       postgresql90-server
 
-PGSQL_CLIENT_DEBS_83 = \
+PGSQL_CLIENT_DEBS_90 = \
        postgresql-client
 
-PGSQL_CLIENT_DEBS_84 = \
-       postgresql-client-8.4
-
-PGSQL_SERVER_DEBS_84 = \
-       postgresql-8.4 \
-       postgresql-contrib-8.4 \
-       postgresql-plperl-8.4 \
-       postgresql-server-dev-8.4
+PGSQL_SERVER_DEBS_90 = \
+       postgresql-9.0 \
+       postgresql-contrib-9.0 \
+       postgresql-plperl-9.0 \
+       postgresql-server-dev-9.0
 
 DEB_APACHE_MODS = \
     expires\
@@ -271,19 +262,14 @@ centos: install_centos_pgsql centos_like
 rhel: install_redhat_pgsql centos_like
 centos_like: install_centos_rpms install_yaz install_cpan_marc install install_centos_perl create_ld_local install_cpan_safe install_cpan_force
 
-fedora13: install_fedora_13_rpms install_cpan install_cpan_fedora install_cpan_marc install_js_sm install_cpan_force
-fedora14: fedora13
+fedora14: install_fedora_rpms install_cpan install_cpan_fedora install_cpan_marc install_js_sm install_cpan_force
 
-debian-lenny: lenny generic_debian install_cpan_more install_cpan_safe
 debian-squeeze: squeeze generic_debian
-lenny: install_pgsql_client_debs_83 install_extra_debs
-squeeze: install_pgsql_client_debs_84  install_extra_debs_squeeze
+squeeze: install_pgsql_client_debs_90  install_extra_debs_squeeze
 generic_debian:  install_debs test_for_libdbi_pkg install debian_sys_config install_cpan_force
 
-ubuntu-hardy: hardy generic_ubuntu
 ubuntu-lucid: lucid generic_ubuntu
-hardy: install_pgsql_client_debs_82 install_yaz install_cpan_marc install_extra_encode
-lucid: install_pgsql_client_debs_84 install_extra_debs
+lucid: install_pgsql_client_debs_90 install_extra_debs
 generic_ubuntu: install_debs test_for_libdbi_pkg install debian_sys_config install_cpan_more install_cpan_safe install_cpan_force
 
 # - COMMON TARGETS ---------------------------------------------------------
@@ -311,7 +297,7 @@ install_cpan_safe:
 
 # Install the CPAN modules for Fedora 13
 install_cpan_fedora: 
-       for m in $(FEDORA_13_CPAN); do \
+       for m in $(FEDORA_CPAN); do \
                echo "force install $$m" | perl -MCPAN -e shell;\
        done
 
@@ -392,17 +378,11 @@ debian_sys_config:
 install_debs:
        $(APT_TOOL) install $(DEBS)
 
-install_pgsql_client_debs_84:
-       $(APT_TOOL) install $(PGSQL_CLIENT_DEBS_84)
-
-install_pgsql_server_debs_84:
-       $(APT_TOOL) install $(PGSQL_SERVER_DEBS_84)
-
-install_pgsql_client_debs_83:
-       $(APT_TOOL) install $(PGSQL_CLIENT_DEBS_83)
+install_pgsql_client_debs_90:
+       $(APT_TOOL) install $(PGSQL_CLIENT_DEBS_90)
 
-install_pgsql_client_debs_82:
-       $(APT_TOOL) install $(PGSQL_CLIENT_DEBS_82)
+install_pgsql_server_debs_90:
+       $(APT_TOOL) install $(PGSQL_SERVER_DEBS_90)
 
 # Install the debian-specific dependencies for more modern distros
 install_extra_debs_squeeze: install_extra_debs
@@ -418,11 +398,11 @@ install_extra_encode:
 # ------------------------------------------------------------------
 
 # FEDORA 13
-install_fedora_13_rpms:
+install_fedora_rpms:
        yum -y update
-       yum -y install $(FEDORA_13_RPMS)
+       yum -y install $(FEDORA_RPMS)
 
-install_fedora_13_pgsql_server:
+install_fedora_pgsql_server:
        yum -y install $(PGSQL_84_RPMS)
 
 # CENTOS
index 525871d..152bb50 100644 (file)
@@ -675,7 +675,7 @@ sub _build_volume_list {
 
         my $copies = $e->search_asset_copy([
             { call_number => $volume->id , deleted => 'f' },
-            { flesh => 1, flesh_fields => { acp => ['parts'] } }
+            { flesh => 1, flesh_fields => { acp => ['stat_cat_entries','parts'] } }
         ]);
 
         $copies = [ sort { $a->barcode cmp $b->barcode } @$copies  ];
index e4bb90f..95227e9 100644 (file)
@@ -3226,6 +3226,7 @@ sub do_renew {
     my $circ = $self->editor->search_action_circulation({
         target_copy => $self->copy->id,
         xact_finish => undef,
+        checkin_time => undef,
         ($usrid ? (usr => $usrid) : ()),
         '-or' => [
             {stop_fines => undef},
index 6ce8691..f56a52d 100644 (file)
@@ -795,6 +795,16 @@ sub update_hold_impl {
         return OpenILS::Event->new('BAD_PARAMS') if $hold->fulfillment_time;
         return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
     }
+    
+       
+       # --------------------------------------------------------------
+       # Code for making sure staff have appropriate permissons for cut_in_line
+       # This, as is, doesn't prevent a user from cutting their own holds in line 
+       # but needs to
+       # --------------------------------------------------------------        
+       if($U->is_true($hold->cut_in_line) ne $U->is_true($orig_hold->cut_in_line)) {
+               return $e->die_event unless $e->allowed('UPDATE_HOLD_REQUEST_TIME', $hold->pickup_lib);
+       }
 
     # --------------------------------------------------------------
     # if the hold is on the holds shelf or in transit and the pickup 
index 5cd7087..4be3a41 100644 (file)
@@ -2012,7 +2012,7 @@ __PACKAGE__->register_method(
     method   => "copy_count_summary",
     api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
     notes    => "returns an array of these: "
-              . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
+              . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
               . "where statusx is a copy status name.  The statuses are sorted by ID.",
 );
                
@@ -2024,14 +2024,18 @@ sub copy_count_summary {
     my $data = $U->storagereq(
                'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
 
-    return [ sort { $a->[1] cmp $b->[1] } @$data ];
+    return [ sort {
+        (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
+        cmp
+        (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
+    } @$data ];
 }
 
 __PACKAGE__->register_method(
     method   => "copy_location_count_summary",
     api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
     notes    => "returns an array of these: "
-              . "[ org_id, callnumber_label, copy_location, <status1_count>, <status2_count>,...] "
+              . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
               . "where statusx is a copy status name.  The statuses are sorted by ID.",
 );
 
@@ -2042,14 +2046,20 @@ sub copy_location_count_summary {
     my $data = $U->storagereq(
                'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
 
-    return [ sort { $a->[1] cmp $b->[1] || $a->[2] cmp $b->[2] } @$data ];
+    return [ sort {
+        (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
+        cmp
+        (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
+
+        || $a->[4] cmp $b->[4]
+    } @$data ];
 }
 
 __PACKAGE__->register_method(
     method   => "copy_count_location_summary",
     api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
     notes    => "returns an array of these: "
-              . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
+              . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
               . "where statusx is a copy status name.  The statuses are sorted by ID."
 );
 
@@ -2059,7 +2069,11 @@ sub copy_count_location_summary {
     $depth ||= 0;
     my $data = $U->storagereq(
         'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
-    return [ sort { $a->[1] cmp $b->[1] } @$data ];
+    return [ sort {
+        (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
+        cmp
+        (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
+    } @$data ];
 }
 
 
@@ -2620,9 +2634,11 @@ __PACKAGE__->register_method(
 );
 
 sub copies_by_cn_label {
-       my( $self, $conn, $record, $label, $circ_lib ) = @_;
+       my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
        my $e = new_editor();
-       my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
+    my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
+    my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
+       my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
        return [] unless @$cns;
 
        # show all non-deleted copies in the staff client ...
index 06d2f63..a08a8c3 100644 (file)
@@ -125,8 +125,12 @@ sub bib_to_svr {
        my $mfhd_parser = OpenILS::Utils::MFHDParser->new();
        foreach (@$sdists) {
         my $svr;
-        if (ref $_->record_entry) {
-            $svr = $mfhd_parser->generate_svr($_->record_entry->id, $_->record_entry->marc, $_->record_entry->owning_lib);
+        if (ref $_->record_entry and $_->summary_method ne 'use_sdist_only') {
+            my $skip_all_computable = 0;
+            if ($_->summary_method eq 'merge_with_sre') { # 'computable' (85x/86x combos) are handled by generated_coverage when attempting to merge
+                $skip_all_computable = 1;
+            }
+            $svr = $mfhd_parser->generate_svr($_->record_entry->id, $_->record_entry->marc, $_->record_entry->owning_lib, $skip_all_computable);
         } else {
             $svr = Fieldmapper::serial::virtual_record->new;
             $svr->sre_id(-1);
@@ -142,28 +146,30 @@ sub bib_to_svr {
             $svr->missing([]);
             $svr->incomplete([]);
         }
-        if (ref $_->basic_summary) { #TODO: 'show-generated' boolean on summaries
-            if ($_->basic_summary->generated_coverage) {
-                push(@{$svr->basic_holdings}, $_->basic_summary->generated_coverage);
-            }
-            if ($_->basic_summary->textual_holdings) {
-                push(@{$svr->basic_holdings_add}, $_->basic_summary->textual_holdings);
-            }
-        }
-        if (ref $_->supplement_summary) {
-            if ($_->supplement_summary->generated_coverage) {
-                push(@{$svr->supplement_holdings}, $_->supplement_summary->generated_coverage);
-            }
-            if ($_->supplement_summary->textual_holdings) {
-                push(@{$svr->supplement_holdings_add}, $_->supplement_summary->textual_holdings);
+        if ($_->summary_method ne 'use_sre_only') {
+            if (ref $_->basic_summary) { #TODO: 'show-generated' boolean on summaries
+                if ($_->basic_summary->generated_coverage) {
+                    push(@{$svr->basic_holdings}, $_->basic_summary->generated_coverage);
+                }
+                if ($_->basic_summary->textual_holdings) {
+                    push(@{$svr->basic_holdings_add}, $_->basic_summary->textual_holdings);
+                }
             }
-        }
-        if (ref $_->index_summary) {
-            if ($_->index_summary->generated_coverage) {
-                push(@{$svr->index_holdings}, $_->index_summary->generated_coverage);
+            if (ref $_->supplement_summary) {
+                if ($_->supplement_summary->generated_coverage) {
+                    push(@{$svr->supplement_holdings}, $_->supplement_summary->generated_coverage);
+                }
+                if ($_->supplement_summary->textual_holdings) {
+                    push(@{$svr->supplement_holdings_add}, $_->supplement_summary->textual_holdings);
+                }
             }
-            if ($_->index_summary->textual_holdings) {
-                push(@{$svr->index_holdings_add}, $_->index_summary->textual_holdings);
+            if (ref $_->index_summary) {
+                if ($_->index_summary->generated_coverage) {
+                    push(@{$svr->index_holdings}, $_->index_summary->generated_coverage);
+                }
+                if ($_->index_summary->textual_holdings) {
+                    push(@{$svr->index_holdings_add}, $_->index_summary->textual_holdings);
+                }
             }
         }
         push(@$svrs, $svr);
index 2ee5cba..832d0c2 100644 (file)
@@ -247,13 +247,26 @@ sub fleshed_item_alter {
     my $editor = new_editor(requestor => $reqr, xact => 1);
     my $override = $self->api_name =~ /override/;
 
-# TODO: permission check
-#        return $editor->event unless
-#            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
-
+    my %found_sdist_ids;
+    my %found_sstr_ids;
     for my $item (@$items) {
+        my $sstr_id = ref $item->stream ? $item->stream->id : $item->stream;
+        if (!exists($found_sstr_ids{$sstr_id})) {
+            my $sstr;
+            if (ref $item->stream) {
+                $sstr = $item->stream;
+            } else {
+                $sstr = $editor->retrieve_serial_stream($item->stream) or return $editor->die_event;
+            }
+            if (!exists($found_sdist_ids{$sstr->distribution})) {
+                my $sdist = $editor->retrieve_serial_distribution($sstr->distribution) or return $editor->die_event;
+                return $editor->die_event unless
+                    $editor->allowed("ADMIN_SERIAL_STREAM", $sdist->holding_lib);
+                $found_sdist_ids{$sstr->distribution} = 1;
+            }
+            $found_sstr_ids{$sstr_id} = 1;
+        }
 
-        my $itemid = $item->id;
         $item->editor($editor->requestor->id);
         $item->edit_date('now');
 
@@ -370,11 +383,22 @@ sub fleshed_issuance_alter {
     my $editor = new_editor(requestor => $reqr, xact => 1);
     my $override = $self->api_name =~ /override/;
 
-# TODO: permission support
-#        return $editor->event unless
-#            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
-
+    my %found_ssub_ids;
     for my $issuance (@$issuances) {
+        my $ssub_id = ref $issuance->subscription ? $issuance->subscription->id : $issuance->subscription;
+        if (!exists($found_ssub_ids{$ssub_id})) {
+            my $owning_lib_id;
+            if (ref $issuance->subscription) {
+                $owning_lib_id = $issuance->subscription->owning_lib;
+            } else {
+                my $ssub = $editor->retrieve_serial_subscription($issuance->subscription) or return $editor->die_event;
+                $owning_lib_id = $ssub->owning_lib;
+            }
+            return $editor->die_event unless
+                $editor->allowed("ADMIN_SERIAL_SUBSCRIPTION", $owning_lib_id);
+            $found_ssub_ids{$ssub_id} = 1;
+        }
+
         my $issuanceid = $issuance->id;
         $issuance->editor($editor->requestor->id);
         $issuance->edit_date('now');
@@ -676,11 +700,22 @@ sub fleshed_sunit_alter {
     my $editor = new_editor(requestor => $reqr, xact => 1);
     my $override = $self->api_name =~ /override/;
 
-# TODO: permission support
-#        return $editor->event unless
-#            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
-
+    my %found_cn_ids;
     for my $sunit (@$sunits) {
+        my $cn_id = ref $sunit->call_number ? $sunit->call_number->id : $sunit->call_number;
+        if (!exists($found_cn_ids{$cn_id})) {
+            my $owning_lib_id;
+            if (ref $sunit->call_number) {
+                $owning_lib_id = $sunit->call_number->owning_lib;
+            } else {
+                my $cn = $editor->retrieve_asset_call_number($sunit->call_number) or return $editor->die_event;
+                $owning_lib_id = $cn->owning_lib;
+            }
+            return $editor->die_event unless
+                $editor->allowed("UPDATE_COPY", $owning_lib_id);
+            $found_cn_ids{$cn_id} = 1;
+        }
+
         if( $sunit->isdeleted ) {
             $evt = _delete_sunit( $editor, $override, $sunit );
         } else {
@@ -903,7 +938,8 @@ sub make_predictions {
         push (@issuances, $issuance);
     }
 
-    fleshed_issuance_alter($self, $conn, $authtoken, \@issuances); # FIXME: catch events
+    my $evt = fleshed_issuance_alter($self, $conn, $authtoken, \@issuances);
+    return $evt if ref $evt;
 
     my @items;
     for (my $i = 0; $i < @issuances; $i++) {
@@ -932,8 +968,6 @@ sub make_predictions {
 # a hash ref of options initially defined as:
 # caption : the caption field to predict on
 # num_to_predict : the number of issues you wish to predict
-# last_rec_date : the date of the last received issue, to be used as an offset
-#                 for predicting future issues
 # faked_chron_date : if the serial does not actually have a chronology caption (but we need one for prediction's sake), base predictions on this date
 #
 # The basic method is to first convert to a single holding if compressed, then
@@ -952,7 +986,6 @@ sub _generate_issuance_values {
     my $end_date = $options->{end_date};
     my $predict_from = $options->{predict_from};   # issuance to predict from
     my $faked_chron_date = $options->{faked_chron_date};   # serial does not have a chronology caption, so add one (temporarily) based on this date 
-    #my $last_rec_date = $options->{last_rec_date};   # expected or actual
 
 
 # Only needed for 'real' MFHD records, not our temp records
@@ -1040,6 +1073,8 @@ sub _revive_holding {
 
     # build MFHD::Holding
     return new MFHD::Holding($seqno, $holding_field, $caption_field);
+
+    # TODO(?) the underlying MARC and the Holding object end up in conflict concerning subfield '8'
 }
 
 __PACKAGE__->register_method(
@@ -1237,11 +1272,13 @@ sub unitize_items {
 
         $found_stream_ids{$stream_id} = 1;
 
-        if (defined($unit_id)) {
+        if (defined($unit_id) and $unit_id ne '') {
             $found_unit_ids{$unit_id} = 1;
             # save the stream_id for this unit_id
             # TODO: prevent items from different streams in same unit? (perhaps in interface)
             $stream_ids_by_unit_id{$unit_id} = $stream_id;
+        } else {
+            $item->clear_unit;
         }
 
         my $evt = _update_sitem($editor, undef, $item);
@@ -1324,6 +1361,7 @@ sub unitize_items {
             } else {
                 $sdist = $editor->search_serial_distribution([{"+sstr" => {"id" => $stream_id}}, { "join" => {"sstr" => {}} }]);
                 $sdist = $sdist->[0];
+                $sdist_by_stream_id{$stream_id} = $sdist;
             }
             my $streams;
             if (!exists($streams_by_sdist{$sdist->id})) {
@@ -1344,7 +1382,7 @@ sub unitize_items {
             foreach my $type (keys %{$found_types{$stream_id}}) {
                 my $issuances = $editor->search_serial_issuance([ {"+sitem" => {"stream" => $stream_id, "status" => "Received"}, "+scap" => {"type" => $type}}, {"join" => {"sitem" => {}, "scap" => {}}, "order_by" => {"siss" => "date_published"}} ]);
                 #TODO: evt on search failure
-                my $evt = _prepare_summaries($editor, $issuances, $sdist_id, $type);
+                my $evt = _prepare_summaries($editor, $issuances, $sdist_by_stream_id{$stream_id}, $type);
                 if ($U->event_code($evt)) {
                     $editor->rollback;
                     return $evt;
@@ -1459,12 +1497,12 @@ sub _prepare_unit {
 # type ('basic', 'index', 'supplement') for a given distribution.
 # It also creates the summary if it doesn't yet exist.
 sub _prepare_summaries {
-    my ($e, $issuances, $dist_id, $type) = @_;
+    my ($e, $issuances, $sdist, $type) = @_;
 
-    my ($mfhd, $formatted_parts) = _summarize_contents($e, $issuances);
+    my ($mfhd, $formatted_parts) = _summarize_contents($e, $issuances, $sdist);
 
     my $search_method = "search_serial_${type}_summary";
-    my $summary = $e->$search_method([{"distribution" => $dist_id}]);
+    my $summary = $e->$search_method([{"distribution" => $sdist->id}]);
 
     my $cu_method = "update";
 
@@ -1473,7 +1511,7 @@ sub _prepare_summaries {
     } else {
         my $class = "Fieldmapper::serial::${type}_summary";
         $summary = $class->new;
-        $summary->distribution($dist_id);
+        $summary->distribution($sdist->id);
         $cu_method = "create";
     }
 
@@ -1660,7 +1698,7 @@ sub receive_items_one_unit_per {
             unless (grep { $_->id == $item->issuance->id } @$issuances_received) {
                 push @$issuances_received, $item->issuance;
             }
-            $evt = _prepare_summaries($e, $issuances_received, $item->stream->distribution->id, $item->issuance->holding_type);
+            $evt = _prepare_summaries($e, $issuances_received, $item->stream->distribution, $item->issuance->holding_type);
             if ($U->event_code($evt)) {
                 $e->rollback;
                 return $evt;
@@ -1751,14 +1789,32 @@ sub _build_unit {
 sub _summarize_contents {
     my $editor = shift;
     my $issuances = shift;
+    my $sdist = shift;
+
+    # create or lookup MFHD record
+    my $mfhd;
+    if ($sdist and defined($sdist->record_entry) and $sdist->summary_method eq 'merge_with_sre') {
+        my $sre;
+        if (ref $sdist->record_entry) {
+            $sre = $sdist->record_entry; 
+        } else {
+            $sre = $editor->retrieve_serial_record_entry($sdist->record_entry);
+        }
+        $mfhd = MFHD->new(MARC::Record->new_from_xml($sre->marc)); 
+    } else {
+        $logger->info($sdist);
+        $mfhd = MFHD->new(MARC::Record->new());
+    }
 
-    # create MFHD record
-    my $mfhd = MFHD->new(MARC::Record->new());
     my %scaps;
     my %scap_fields;
-    my @scap_fields_ordered;
     my $seqno = 1;
-    my $link_id = 1;
+    # We keep track of these separately to avoid link_id contamination,
+    # e.g. a basic issuance, followed by a merging supplement, followed by
+    # another basic.  If we could be sure that they were not mixed, one
+    # value could suffice.
+    my %link_ids = ('basic' => 10000, 'index' => 10000, 'supplement' => 10000);
+    my %first_scap = ('basic' => 1, 'index' => 1, 'supplement' => 1);
     foreach my $issuance (@$issuances) {
         my $scap_id = $issuance->caption_and_pattern;
         next if (!$scap_id); # skip issuances with no caption/pattern
@@ -1770,13 +1826,35 @@ sub _summarize_contents {
             $scaps{$scap_id} = $editor->retrieve_serial_caption_and_pattern($scap_id);
             $scap = $scaps{$scap_id};
             $scap_field = _revive_caption($scap);
+            my $did_merge = 0;
+            if ($first_scap{$scap->type}) { # special merge processing
+                $first_scap{$MFHD_TAGS_BY_NAME{$scap->type}} = 0;
+                if ($sdist and $sdist->summary_method eq 'merge_with_sre') {
+                    # MFHD Caption objects do not yet have a built-in compare (TODO), so let's do a basic one
+                    my @field_85xs = $mfhd->field($MFHD_TAGS_BY_NAME{$scap->type});
+                    if (@field_85xs) {
+                        my $last_caption_field = $field_85xs[-1];
+                        my $last_link_id = $last_caption_field->subfield('8');
+                        # set the link id to match, temporarily, for comparison
+                        $last_caption_field->update('8' => $scap_field->subfield('8'));
+                        my $last_caption_json = OpenSRF::Utils::JSON->perl2JSON([$last_caption_field->indicator(1), $last_caption_field->indicator(2), $last_caption_field->subfields_list]);
+                        if ($last_caption_json eq $scap->pattern_code) { # merge is possible, they match
+                            # restore link id
+                            $link_ids{$scap->type} = $last_link_id;
+                            # set scap_field to last field
+                            $scap_field = $last_caption_field;
+                            $did_merge = 1;
+                        }
+                    }
+                }
+            }
             $scap_fields{$scap_id} = $scap_field;
-            push(@scap_fields_ordered, $scap_field);
-            $scap_field->update('8' => $link_id);
-            $mfhd->append_fields($scap_field);
-            $link_id++;
+            $scap_field->update('8' => $link_ids{$scap->type});
+            # TODO: make MFHD/Caption smarter about this
+            $scap_field->{_mfhdc_LINK_ID} = $link_ids{$scap->type};
+            $mfhd->append_fields($scap_field) if !$did_merge;
+            $link_ids{$scap->type}++;
         } else {
-            $scap = $scaps{$scap_id};
             $scap_field = $scap_fields{$scap_id};
         }
 
@@ -1785,6 +1863,7 @@ sub _summarize_contents {
     }
 
     my @formatted_parts;
+    my @scap_fields_ordered = $mfhd->field('85[345]');
     foreach my $scap_field (@scap_fields_ordered) { #TODO: use generic MFHD "summarize" method, once available
        my @updated_holdings = $mfhd->get_compressed_holdings($scap_field);
        foreach my $holding (@updated_holdings) {
@@ -2023,11 +2102,10 @@ sub fleshed_ssub_alter {
     my $editor = new_editor(requestor => $reqr, xact => 1);
     my $override = $self->api_name =~ /override/;
 
-# TODO: permission check
-#        return $editor->event unless
-#            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
-
     for my $ssub (@$ssubs) {
+        my $owning_lib_id = ref $ssub->owning_lib ? $ssub->owning_lib->id : $ssub->owning_lib;
+        return $editor->die_event unless
+            $editor->allowed("ADMIN_SERIAL_SUBSCRIPTION", $owning_lib_id);
 
         my $ssubid = $ssub->id;
 
@@ -2250,12 +2328,10 @@ sub fleshed_sdist_alter {
     my $editor = new_editor(requestor => $reqr, xact => 1);
     my $override = $self->api_name =~ /override/;
 
-# TODO: permission check
-#        return $editor->event unless
-#            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
-
     for my $sdist (@$sdists) {
-        my $sdistid = $sdist->id;
+        my $holding_lib_id = ref $sdist->holding_lib ? $sdist->holding_lib->id : $sdist->holding_lib;
+        return $editor->die_event unless
+            $editor->allowed("ADMIN_SERIAL_DISTRIBUTION", $holding_lib_id);
 
         if( $sdist->isdeleted ) {
             $evt = _delete_sdist( $editor, $override, $sdist);
@@ -2454,12 +2530,14 @@ sub scap_alter {
     my $editor = new_editor(requestor => $reqr, xact => 1);
     my $override = $self->api_name =~ /override/;
 
-# TODO: permission check
-#        return $editor->event unless
-#            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
-
+    my %found_ssub_ids;
     for my $scap (@$scaps) {
-        my $scapid = $scap->id;
+        if (!exists($found_ssub_ids{$scap->subscription})) {
+            my $ssub = $editor->retrieve_serial_subscription($scap->subscription) or return $editor->die_event;
+            return $editor->die_event unless
+                $editor->allowed("ADMIN_SERIAL_CAPTION_PATTERN", $ssub->owning_lib);
+            $found_ssub_ids{$scap->subscription} = 1;
+        }
 
         if( $scap->isdeleted ) {
             $evt = _delete_scap( $editor, $override, $scap);
@@ -2565,12 +2643,14 @@ sub sstr_alter {
     my $editor = new_editor(requestor => $reqr, xact => 1);
     my $override = $self->api_name =~ /override/;
 
-# TODO: permission check
-#        return $editor->event unless
-#            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
-
+    my %found_sdist_ids;
     for my $sstr (@$sstrs) {
-        my $sstrid = $sstr->id;
+        if (!exists($found_sdist_ids{$sstr->distribution})) {
+            my $sdist = $editor->retrieve_serial_distribution($sstr->distribution) or return $editor->die_event;
+            return $editor->die_event unless
+                $editor->allowed("ADMIN_SERIAL_STREAM", $sdist->holding_lib);
+            $found_sdist_ids{$sstr->distribution} = 1;
+        }
 
         if( $sstr->isdeleted ) {
             $evt = _delete_sstr( $editor, $override, $sstr);
@@ -2733,12 +2813,14 @@ sub sum_alter {
     my $editor = new_editor(requestor => $reqr, xact => 1);
     my $override = $self->api_name =~ /override/;
 
-# TODO: permission check
-#        return $editor->event unless
-#            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
-
+    my %found_sdist_ids;
     for my $sum (@$sums) {
-        my $sumid = $sum->id;
+        if (!exists($found_sdist_ids{$sum->distribution})) {
+            my $sdist = $editor->retrieve_serial_distribution($sum->distribution) or return $editor->die_event;
+            return $editor->die_event unless
+                $editor->allowed("ADMIN_SERIAL_DISTRIBUTION", $sdist->holding_lib);
+            $found_sdist_ids{$sum->distribution} = 1;
+        }
 
         # XXX: (for now, at least) summaries should be created/deleted by the distribution functions
         if( $sum->isdeleted ) {
index 1915424..b858fd5 100644 (file)
@@ -585,6 +585,13 @@ sub toSQL {
 
     my $core_limit = $self->QueryParser->core_limit || 25000;
 
+    my $flat_where = $$flat_plan{where};
+    if ($flat_where eq '()') {
+        $flat_where = '';
+    } else {
+        $flat_where = "AND $flat_where";
+    }
+
     my $sql = <<SQL;
 SELECT  $key AS id,
         ARRAY_ACCUM(DISTINCT m.source) AS records,
@@ -600,7 +607,7 @@ SELECT  $key AS id,
         $during
         $between
         $combined_dyn_filters
-        AND $$flat_plan{where}
+        $flat_where
   GROUP BY 1
   ORDER BY 4 $desc NULLS LAST, 5 DESC NULLS LAST, 3 DESC
   LIMIT $core_limit
index cd3d4b5..75152ce 100644 (file)
@@ -197,8 +197,10 @@ sub calc_proximity {
                                actor.org_unit r;
        SQL
 
+       $self->method_lookup('open-ils.storage.transaction.begin')->run;
        actor::org_unit_proximity->db_Main->do($delete_sql);
        actor::org_unit_proximity->db_Main->do($insert_sql);
+       $self->method_lookup('open-ils.storage.transaction.commit')->run;
 
        return 1;
 }
@@ -680,7 +682,11 @@ sub patron_search {
        my @phonev;
        if ($pv) {
                for my $p ( qw/day_phone evening_phone other_phone/ ) {
-                       push @ps, "evergreen.lowercase($p) ~ ?";
+                       if ($pv =~ /^\d+$/) {
+                               push @ps, "evergreen.lowercase(REGEXP_REPLACE($p, '[^0-9]', '', 'g')) ~ ?";
+                       } else {
+                               push @ps, "evergreen.lowercase($p) ~ ?";
+                       }
                        push @phonev, "^$pv";
                }
                $phone = '(' . join(' OR ', @ps) . ')';
index 7c7c830..b6dd586 100644 (file)
@@ -381,7 +381,7 @@ sub record_copy_status_count {
 
        my $cn_table = asset::call_number->table;
        my $cnp_table = asset::call_number_prefix->table;
-       my $cns_table = asset::call_number_prefix->table;
+       my $cns_table = asset::call_number_suffix->table;
        my $cp_table = asset::copy->table;
        my $cl_table = asset::copy_location->table;
        my $cs_table = config::copy_status->table;
@@ -389,7 +389,9 @@ sub record_copy_status_count {
        my $sql = <<"   SQL";
 
                SELECT  cp.circ_lib,
-                               CASE WHEN cnp.id > -1 THEN cnp.label || ' ' ELSE '' END || cn.label || CASE WHEN cns.id > -1 THEN ' ' || cns.label ELSE '' END,
+                               CASE WHEN cnp.id > -1 THEN cnp.label ELSE '' END,
+                cn.label,
+                CASE WHEN cns.id > -1 THEN cns.label ELSE '' END,
                                cp.status,
                                count(cp.id)
                  FROM  $cp_table cp,
@@ -410,7 +412,7 @@ sub record_copy_status_count {
                        AND cp.opac_visible IS TRUE
                        AND cp.deleted IS FALSE
                        AND cs.opac_visible IS TRUE
-                 GROUP BY 1,2,3;
+                 GROUP BY 1,2,3,4,5;
        SQL
 
        my $sth = biblio::record_entry->db_Main->prepare_cached($sql);
@@ -418,13 +420,17 @@ sub record_copy_status_count {
 
        my %data = ();
        for my $row (@{$sth->fetchall_arrayref}) {
-               $data{$$row[0]}{$$row[1]}{$$row[2]} += $$row[3];
+               $data{$$row[0]}{$$row[1]}{$$row[2]}{$$row[3]}{$$row[4]} += $$row[5];
        }
        
        for my $ou (keys %data) {
-               for my $cn (keys %{$data{$ou}}) {
-                       $client->respond( [$ou, $cn, $data{$ou}{$cn}] );
-               }
+               for my $cn_prefix (keys %{$data{$ou}}) {
+                   for my $cn (keys %{$data{$ou}{$cn_prefix}}) {
+                       for my $cn_suffix (keys %{$data{$ou}{$cn_prefix}{$cn}}) {
+                               $client->respond( [$ou, $cn_prefix, $cn, $cn_suffix, $data{$ou}{$cn}{$cn_prefix}{$cn}{$cn_suffix}] );
+                       }
+            }
+        }
        }
        return undef;
 }
@@ -450,7 +456,7 @@ sub record_copy_status_location_count {
 
        my $cn_table = asset::call_number->table;
        my $cnp_table = asset::call_number_prefix->table;
-       my $cns_table = asset::call_number_prefix->table;
+       my $cns_table = asset::call_number_suffix->table;
        my $cp_table = asset::copy->table;
        my $cl_table = asset::copy_location->table;
        my $cs_table = config::copy_status->table;
@@ -461,7 +467,9 @@ sub record_copy_status_location_count {
        my $sql = <<"   SQL";
 
                SELECT  cp.circ_lib,
-                               CASE WHEN cnp.id > -1 THEN cnp.label || ' ' ELSE '' END || cn.label || CASE WHEN cns.id > -1 THEN ' ' || cns.label ELSE '' END,
+                               CASE WHEN cnp.id > -1 THEN cnp.label ELSE '' END,
+                cn.label,
+                CASE WHEN cns.id > -1 THEN cns.label ELSE '' END,
                                oils_i18n_xlate('asset.copy_location', 'acpl', 'name', 'id', cl.id::TEXT, ?),
                                cp.status,
                                count(cp.id)
@@ -483,7 +491,7 @@ sub record_copy_status_location_count {
                        AND cp.opac_visible IS TRUE
                        AND cp.deleted IS FALSE
                        AND cs.opac_visible IS TRUE
-                 GROUP BY 1,2,3,4;
+                 GROUP BY 1,2,3,4,5,6;
        SQL
 
        my $sth = biblio::record_entry->db_Main->prepare_cached($sql);
@@ -492,14 +500,18 @@ sub record_copy_status_location_count {
 
        my %data = ();
        for my $row (@{$sth->fetchall_arrayref}) {
-               $data{$$row[0]}{$$row[1]}{$$row[2]}{$$row[3]} += $$row[4];
+               $data{$$row[0]}{$$row[1]}{$$row[2]}{$$row[3]}{$$row[4]}{$$row[5]} += $$row[6];
        }
        
        for my $ou (keys %data) {
-               for my $cn (keys %{$data{$ou}}) {
-                   for my $cl (keys %{$data{$ou}{$cn}}) {
-                       $client->respond( [$ou, $cn, $cl, $data{$ou}{$cn}{$cl}] );
-            }
+               for my $cn_prefix (keys %{$data{$ou}}) {
+                   for my $cn (keys %{$data{$ou}{$cn_prefix}}) {
+                       for my $cn_suffix (keys %{$data{$ou}{$cn_prefix}{$cn}}) {
+                    for my $cl (keys %{$data{$ou}{$cn_prefix}{$cn}{$cn_suffix}}) {
+                        $client->respond( [$ou, $cn_prefix, $cn, $cn_suffix, $cl, $data{$ou}{$cn_prefix}{$cn}{$cn_suffix}{$cl}] );
+                    }
+                       }
+                   }
                }
        }
        return undef;
index 52a203b..1b3a37f 100644 (file)
@@ -546,7 +546,7 @@ sub decompose {
             warn "Encountered explicit group end\n" if $self->debug;
 
             $_ = $';
-            $remainder = $';
+            $remainder = $struct->top_plan ? '' : $';
 
             $last_type = '';
         } elsif ($self->filter_count && /$filter_re/) { # found a filter
@@ -593,7 +593,7 @@ sub decompose {
             warn "Encountered explicit group start\n" if $self->debug;
 
             my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
-            $struct->add_node( $substruct );
+            $struct->add_node( $substruct ) if ($substruct);
             $_ = $subremainder;
 
             $last_type = '';
@@ -685,6 +685,8 @@ sub decompose {
 
     }
 
+    $struct = undef if (scalar(@{$struct->query_nodes}) == 0 && !$struct->top_plan);
+
     return $struct if !wantarray;
     return ($struct, $remainder);
 }
index a411b21..5c48282 100644 (file)
@@ -1040,7 +1040,7 @@ sub import_record_asset_list_impl {
             $copy->barcode($item->barcode);
             $copy->location($item->location);
             $copy->circ_lib($item->circ_lib || $item->owning_lib);
-            $copy->status($item->status || OILS_COPY_STATUS_IN_PROCESS);
+            $copy->status( defined($item->status) ? $item->status : OILS_COPY_STATUS_IN_PROCESS );
             $copy->circulate($item->circulate);
             $copy->deposit($item->deposit);
             $copy->deposit_amount($item->deposit_amount);
index 607dce7..c628820 100644 (file)
@@ -269,7 +269,10 @@ sub language {
 sub charge_ok {
     my $self = shift;
     my $u = $self->{user};
-    return (($u->barred eq 'f') and ($u->card->active eq 't'));
+    return 
+        $u->barred eq 'f' and 
+        $u->active eq 't' and
+        $u->card->active eq 't';
 }
 
 # How much more detail do we need to check here?
index 55e6df9..9445834 100644 (file)
@@ -101,17 +101,6 @@ sub new {
 
     my $pat = $self->{_mfhdc_PATTERN};
 
-    # Sanity check publication frequency vs publication pattern:
-    # if the frequency is a number, then the pattern better
-    # have that number of values associated with it.
-    if (   exists($pat->{w})
-        && ($pat->{w} =~ /^\d+$/)
-        && ($pat->{w} != scalar(@{$pat->{y}->{p}}))) {
-        carp(
-"Caption::new: publication frequency '$pat->{w}' != publication pattern @{$pat->{y}->{p}}"
-        );
-    }
-
     # If there's a $x subfield and a $j, then it's compressible
     if (exists $pat->{x} && exists $self->{_mfhdc_CHRONS}->{'j'}) {
         $self->{_mfhdc_COMPRESSIBLE} = 1;
@@ -397,14 +386,15 @@ sub next_chron {
     my $freq    = $pattern->{w};
 
     foreach my $i (0..$#keys) {
-        $cur[$i] = $next->{$keys[$i]} if exists $next->{$keys[$i]};
+        if (exists $next->{$keys[$i]}) {
+            $cur[$i] = $next->{$keys[$i]};
+            # If the current issue has a combined date (eg, May/June)
+            # get rid of the first date and base the calculation
+            # on the final date in the combined issue.
+            $cur[$i] =~ s|^[^/]+/||;
+        }
     }
 
-    # If the current issue has a combined date (eg, May/June)
-    # get rid of the first date and base the calculation
-    # on the final date in the combined issue.
-    $cur[-1] =~ s|^[^/]+/||;
-
     if (defined $pattern->{y}->{p}) {
         # There is a $y publication pattern defined in the record:
         # use it to calculate the next issue date.
@@ -434,12 +424,12 @@ sub next_chron {
                     ($start, $end) = (undef, undef);
                 }
 
-                @candidate = $genfunc->($start || $pat, @cur);
+                @candidate = $genfunc->($start || $pat, \@cur, $self);
 
                 while ($self->is_omitted(@candidate)) {
                     #              printf("# pubpat omitting date '%s'\n",
                     #                     join('/', @candidate));
-                    @candidate = $genfunc->($start || $pat, @candidate);
+                    @candidate = $genfunc->($start || $pat, \@candidate, $self);
                 }
 
                 #              printf("# testing new candidate '%s' against '%s'\n",
@@ -451,7 +441,7 @@ sub next_chron {
                     # @candidate is the next issue.
                     @new = @candidate;
                     if (defined $end) {
-                        @newend = $genfunc->($end, @cur);
+                        @newend = $genfunc->($end, \@cur, $self);
                     } else {
                         $newend[0] = undef;
                     }
@@ -461,6 +451,8 @@ sub next_chron {
             }
         }
 
+        $new[1] = 24 if ($new[1] == 20); # restore fake early winter
+
         if (defined($newend[0])) {
             # The best match was a combined issue
             foreach my $i (0..$#new) {
@@ -488,10 +480,11 @@ sub next_chron {
 
             if ($self->is_combined(@new)) {
                 my @second_date = MFHD::Date::incr_date($freq, @new);
-
-                # I am cheating: This code assumes that only the smallest
-                # time increment is combined. So, no "Apr 15/May 1" allowed.
-                $new[-1] = $new[-1] . '/' . $second_date[-1];
+                foreach my $i (0..$#new) {
+                    # don't combine identical fields
+                    next if $new[$i] eq $second_date[$i];
+                    $new[$i] .= '/' . $second_date[$i];
+                }
             }
         }
     }
@@ -499,18 +492,57 @@ sub next_chron {
     for my $i (0..$#new) {
         $next->{$keys[$i]} = $new[$i];
     }
+
     # Figure out if we need to adjust volume number
-    # right now just use the $carry that was passed in.
-    # in long run, need to base this on ($carry or date_change)
-    if ($carry) {
-        # if $carry is set, the date doesn't matter: we're not
-        # going to increment the v. number twice at year-change.
+    #
+    # If we are incrementing based on date, $carry doesn't
+    # matter: we're not going to increment the v. number twice
+    #
+    # It is conceivable that a serial could increment based on date for some
+    # volumes and issue numbering for other volumes, but until a real case
+    # comes up, let's assume that defined calendar changes always trump $u
+    if (defined $pattern->{x}) {
+        my $increment = $self->calendar_increment(\@cur, \@new);
+        # if we hit a calendar change, restart dependant restarters
+        # regardless of whether they thought they should
+        if ($increment) {
+            $next->{a} += $increment;
+            foreach my $key ('b'..'f') {
+                next if !exists $next->{$key};
+                my $cap = $self->capfield($key);
+                if ($cap->{RESTART}) {
+                    $next->{$key} = 1;
+                    if ($self->enum_is_combined($key, $next->{$key})) {
+                        $next->{$key} .= '/' . ($next->{$key} + 1);
+                    }
+                } else {
+                    last; # if we find a non-restarting level, stop
+                }
+            }
+        }
+    } elsif ($carry) {
         $next->{a} += $carry;
-    } elsif (defined $pattern->{x}) {
-        $next->{a} += $self->calendar_increment(\@cur, \@new);
     }
 }
 
+sub winter_starts_year {
+    my $self = shift;
+
+    my $pubpats = $self->{_mfhdc_PATTERN}->{y}->{p};
+    my $freq = $self->{_mfhdc_PATTERN}->{w};
+
+    if ($freq =~ /^\d$/) {
+        foreach my $pubpat (@$pubpats) {
+            my $chroncode = substr($pubpat, 0, 1);
+            if ($chroncode eq 's' and substr($pubpat, 1, 2) == 24) {
+                return 1;        
+            }
+        }
+    }
+    return 0;
+}
+
+
 sub next_alt_enum {
     my $self = shift;
     my $next = shift;
@@ -653,6 +685,15 @@ sub next_enum {
                 && ($next->{$key} eq $cap->{COUNT})) {
                 $next->{$key} = 1;
                 $carry = 1;
+            } elsif ($cap->{COUNT} > 0 and !($next->{$key} % $cap->{COUNT})) {
+                # If we have a non-restarting enum, but we define a count,
+                # we need to carry to the next level when the current value
+                # divides evenly by the count
+                # XXX: this code naively assumes that there has never been an
+                # issue number anomaly of any kind (like an extra issue), but this
+                # limit is inherent in the standard
+                $next->{$key} += 1;
+                $carry = 1;
             } else {
                 # If I don't need to "carry" beyond here, then I just increment
                 # this level of the enumeration and stop looping, since the
index 34c85d9..e442347 100644 (file)
@@ -5,6 +5,7 @@ use Carp;
 
 use Data::Dumper;
 use DateTime;
+use OpenILS::Utils::MFHD::Caption;
 
 use base 'Exporter';
 
@@ -62,9 +63,13 @@ sub match_day {
     }
 }
 
+# TODO: possible support for extraneous $yp information
+# ex. $ypdtu but on a bi-weekly (currently assumes weekly)
 sub subsequent_day {
     my $pat = shift;
-    my @cur = @_;
+    my $cur = shift;
+
+    my @cur = @$cur;
     my $dt  = DateTime->new(
         year  => $cur[0],
         month => $cur[1],
@@ -103,7 +108,7 @@ sub subsequent_day {
         # MMDD: published on the given day of the given month
         my ($mon, $day) = unpack("a2a2", $pat);
 
-        if (on_or_after($mon, $day, $cur[1], $cur[2])) {
+        if (MFHD::Caption::on_or_after([$cur[1], $cur[2]], [$mon, $day])) {
             # Current date is on or after pattern; next one is next year
             $cur[0] += 1;
         }
@@ -280,7 +285,9 @@ sub match_week {
 #
 sub subsequent_week {
     my $pat = shift;
-    my @cur = @_;
+    my $cur = shift;
+
+    my @cur = @$cur;
     my $candidate;
     my $dt;
 
@@ -388,7 +395,9 @@ sub match_month {
 
 sub subsequent_month {
     my $pat = shift;
-    my @cur = @_;
+    my $cur = shift;
+
+    my @cur = @$cur;
 
     if ($cur[1] >= $pat) {
         # Current date is on or after the patter date, so the next
@@ -411,7 +420,10 @@ sub match_season {
 
 sub subsequent_season {
     my $pat = shift;
-    my @cur = @_;
+    my $cur = shift;
+    my $caption = shift;
+
+    my @cur = @$cur;
 
 #     printf("# subsequent_season: pat='%s', cur='%s'\n", $pat, join('/',@cur));
 
@@ -420,6 +432,15 @@ sub subsequent_season {
         return undef;
     }
 
+    if ($caption->winter_starts_year()) {
+        if ($pat == 24) {
+            $pat = 20; # fake early winter
+        }
+        if ($cur[1] == 24) {
+            $cur[1] = 20; # fake early winter
+        }
+    }
+
     if ($cur[1] >= $pat) {
         # current season is on or past pattern season in this year,
         # advance to next year
@@ -445,6 +466,8 @@ sub subsequent_year {
     my $pat = shift;
     my $cur = shift;
 
+    my @cur = @$cur;
+
     # XXX WRITE ME
     return undef;
 }
@@ -463,6 +486,8 @@ sub subsequent_issue {
     my $pat = shift;
     my $cur = shift;
 
+    my @cur = @$cur;
+
     # Issue generation is handled separately
     return undef;
 }
@@ -509,6 +534,7 @@ my %increments = (
     i => {days   => 2},     # three times / week
     j => {days   => 10},    # three times /month
                             # k => continuous
+#    l => {weeks  => 3},     # triweekly (NON-STANDARD)
     m => {months => 1},     # monthly
     q => {months => 3},     # quarterly
     s => {days   => 15},    # semimonthly
index efd6027..ce08d4f 100644 (file)
@@ -362,16 +362,19 @@ sub format_part {
     }
 
     # Breaks in the sequence
-    if (defined($self->{_mfhdh_BREAK})) {
-        if ($self->{_mfhdh_BREAK} eq 'n') {
-            $str .= ' non-gap break';
-        } elsif ($self->{_mfhdh_BREAK} eq 'g') {
-            $str .= ' gap';
-        } else {
-            warn "unrecognized break indicator '$self->{_mfhdh_BREAK}'";
-        }
-    }
-
+# XXX: this is non-standard and also not the right place for this, since gaps
+# only make sense in the context of multiple holding segments, not a single
+# holding
+#    if (defined($self->{_mfhdh_BREAK})) {
+#        if ($self->{_mfhdh_BREAK} eq 'n') {
+#            $str .= ' non-gap break';
+#        } elsif ($self->{_mfhdh_BREAK} eq 'g') {
+#            $str .= ' gap';
+#        } else {
+#            warn "unrecognized break indicator '$self->{_mfhdh_BREAK}'";
+#        }
+#    }
+#
     return $str;
 }
 
@@ -635,13 +638,28 @@ sub chron_to_date {
                     $chrons[$i]->[1] = 9;
                     $chrons[$i]->[2] = 22;
                 } elsif ($seasons[$i] == 24) {
-                    $chrons[$i]->[1] = 12;
-                    $chrons[$i]->[2] = 21;
+                    # "winter" can come at the beginning or end of a year,
+                    if ($self->caption->winter_starts_year()) {
+                        $chrons[$i]->[1] = 1;
+                        $chrons[$i]->[2] = 1;
+                    } else { # default to astronomical
+                        $chrons[$i]->[1] = 12;
+                        $chrons[$i]->[2] = 21;
+                    }
                 }
             }
         }
     }
 
+    # if we have an an annual, set the month to ypm## if available
+    if (exists($self->caption->{_mfhdc_PATTERN}->{y}->{p}) and $self->caption->{_mfhdc_PATTERN}->{w} eq 'a') {
+        my $reg = $self->caption->{_mfhdc_PATTERN}->{y}->{p}->[0];
+        if ($reg =~ /^m(\d+)/) {
+            $chrons[0]->[1] = $1;
+            $chrons[1]->[1] = $1;
+        }
+    }
+
     my @dates;
     foreach my $chron (@chrons) {
         my $date = undef;
index e6dcf44..a478b32 100644 (file)
@@ -54,6 +54,10 @@ while ($rec = testlib::load_MARC_rec($testdata, $testno++)) {
                   if ($field->subfield('z') =~ /^TODO/);
                 is_deeply($field->next, right_answer($field),
                     $field->subfield('8') . ': ' . $field->subfield('z'));
+
+                if ($field->subfield('y')) {
+                    is($field->chron_to_date(), $field->subfield('y'), 'Chron-to-date test');
+                }
             }
         }
     }
index e3b4e1e..883bf3c 100644 (file)
 863 41 $820.3$a1$b5$i1990$j12$x|a2|b1|i1991|j02$zWrap at end of year/vol.
 
 245 00 $aEconomist: pub. w on Sa, except combined iss on last two weeks of year
-853 20 $821$av.$bno.$u12$vc$i(year)$j(month)$k(day)$ww$x01,04,07,10$ypdsa$yow1299
+853 20 $821$av.$bno.$vc$i(year)$j(month)$k(day)$ww$x01,04,07,10$ypdsa$yow1299
 863 41 $821.1$a100$b1200$i2008$j12$k06$x|a100|b1201|i2008|j12|k13$zwithin vol.
 863 41 $821.2$a100$b1201$i2008$j12$k13$x|a100|b1202|i2008|j12|k20$zwithin vol. combined iss.
 863 41 $821.3$a100$b1202$i2008$j12$k20$x|a101|b1203|i2009|j01|k03$zvolume change over omitted iss.
 863 41 $822.4$a2$b4$i2013$j04$k11$x|a2|b5|i2013|j05|k01$zpublished on Wed May 1st
 
 245 00 $aMFHD example: pub. every Mon, Thu, except on New Years, July 4, Labor Day, Thanksgiving, Christmas
-853 20 $823$av.$bno.$uvar$vr$i(year)$j(month)$k(day)$wc$x07$ypw00mo,00th$yod0101,0704,1225$yow0901mo,1104th
+853 20 $823$av.$bno.$uvar$i(year)$j(month)$k(day)$wc$x07$ypw00mo,00th$yod0101,0704,1225$yow0901mo,1104th
 863 41 $823.1$a1$b100$i2009$j02$k02$x|a1|b101|i2009|j02|k05$znormal: Mon to Thu
 863 41 $823.2$a1$b101$i2009$j02$k05$x|a1|b102|i2009|j02|k09$znormal: Thu to Mon
 863 41 $823.3$a1$b150$i2009$j06$k29$x|a2|b151|i2009|j07|k02$znormal: calendar change
 863 41 $826.1$a1$b1$i1990$j01$x|a1|b2|i1990|j02$znormal issue
 864 41 $827.1$a1$i1990$j09$x|a2|i1991|j09$zAnnual supplement
 865 41 $828.1$a1$i1990$j02$x|a2|i1991|j02$zAnnual Index
+
+# Issue numbering restarts at the calendar change
+245 00 $aIssue No. restarts at calendar change
+853 20 $829$av.$bno.$uvar$vr$i(year)$j(month)$k(day)$wd$x0101,0701
+863 41 $829.1$a1$b181$i2011$j06$k30$x|a2|b1|i2011|j07|k01$zJune 30 to July 1
+
+# Winter starts the calendar year
+# Requires a hacky use of MARC due to limitations in MARC standard
+245 00 $aWinter starts the calendar year
+853 20 $830$av.$bno.$u4$vr$i(year)$j(season)$w4$yps24,21,22,23
+863 41 $830.1$a1$b4$i2010$j23$x|a2|b1|i2011|j24$zAutumn 2010 to Winter 2011
+863 41 $830.2$a2$b1$i2011$j24$x|a2|b2|i2011|j21$y2011-01-01$zWinter 2011 to Spring 2011
+
+# Combined seasons
+245 00 $aCombined seasons, and Winter starts the calendar year
+853 20 $831$av.$bno.$u4$vr$i(year)$j(season)$w4$yps24/21,22,23
+863 41 $831.1$a1$b4$i2010$j23$x|a2|b1|i2011|j24/21$zAutumn 2010 to combined Winter/Spring 2011
+
+# Combined seasons, variation
+245 00 $aCombined seasons variation, and Winter starts the calendar year
+853 20 $832$av.$bno.$u4$vr$i(year)$j(season)$w4$yps24,21/22,23
+863 41 $832.1$a1$b4$i2011$j24$x|a2|b1|i2011|j21/22$zWinter 2011 to combined Spring/Summer 2011
+863 41 $832.2$a1$b4$i2011$j21/22$x|a2|b1|i2011|j23$zSpring/Summer 2011 to Autumn 2011
+
+# Defined unit count, non-restarting
+245 00 $aDefined unit count, non-restarting
+853 20 $833$av.$bno.$u4$vc$i(year)$j(month)$wf
+863 41 $833.1$a24$b95$i2011$j01$x|a24|b96|i2011|j07$z3rd Issue to 4th Issue in Volume
+863 41 $833.2$a24$b96$i2011$j07$x|a25|b97|i2012|j01$z4th Issue to 1st Issue in next Volume
+
+# Combined months to end the year
+245 00 $aCombined months to end the year
+853 20 $834$av.$bno.$u12$vr$i(year)$j(month)$wm$ycm12/01$yce22/3
+863 41 $834.1$a24$b1$i2011$j11$x|a24|b2/3|i2011/2012|j12/01$zNov. to Combined Dec./Jan.
+863 41 $834.2$a24$b2/3$i2011/2012$j12/1$x|a24|b4|i2012|j02$zCombined Dec./Jan. to Feb.
index 7a191dd..0540e35 100644 (file)
@@ -54,7 +54,7 @@ Returns a Perl hash containing fields of interest from the MFHD record
 =cut
 
 sub mfhd_to_hash {
-    my ($self, $mfhd_xml) = @_;
+    my ($self, $mfhd_xml, $skip_all_computable) = @_;
 
     my $marc;
     my $mfhd;
@@ -142,55 +142,57 @@ sub mfhd_to_hash {
             }
         }
 
-        if (!exists($skip_computable{'basic'})) {
-            foreach my $cap_id ($mfhd->caption_link_ids('853')) {
-                my @holdings = $mfhd->holdings('863', $cap_id);
-                next unless scalar @holdings;
-                foreach (@holdings) {
-                    push @$basic_holdings, $_->format();
+        if (!$skip_all_computable) {
+            if (!exists($skip_computable{'basic'})) {
+                foreach my $cap_id ($mfhd->caption_link_ids('853')) {
+                    my @holdings = $mfhd->holdings('863', $cap_id);
+                    next unless scalar @holdings;
+                    foreach (@holdings) {
+                        push @$basic_holdings, $_->format();
+                    }
                 }
-            }
-            if (!@$basic_holdings) { # no computed holdings found
+                if (!@$basic_holdings) { # no computed holdings found
+                    $basic_holdings = $basic_holdings_add;
+                    $basic_holdings_add = [];
+                }
+            } else { # textual are non additional, but primary
                 $basic_holdings = $basic_holdings_add;
                 $basic_holdings_add = [];
             }
-        } else { # textual are non additional, but primary
-            $basic_holdings = $basic_holdings_add;
-            $basic_holdings_add = [];
-        }
 
-        if (!exists($skip_computable{'supplement'})) {
-            foreach my $cap_id ($mfhd->caption_link_ids('854')) {
-                my @supplements = $mfhd->holdings('864', $cap_id);
-                next unless scalar @supplements;
-                foreach (@supplements) {
-                    push @$supplement_holdings, $_->format();
+            if (!exists($skip_computable{'supplement'})) {
+                foreach my $cap_id ($mfhd->caption_link_ids('854')) {
+                    my @supplements = $mfhd->holdings('864', $cap_id);
+                    next unless scalar @supplements;
+                    foreach (@supplements) {
+                        push @$supplement_holdings, $_->format();
+                    }
                 }
-            }
-            if (!@$supplement_holdings) { # no computed holdings found
+                if (!@$supplement_holdings) { # no computed holdings found
+                    $supplement_holdings = $supplement_holdings_add;
+                    $supplement_holdings_add = [];
+                }
+            } else { # textual are non additional, but primary
                 $supplement_holdings = $supplement_holdings_add;
                 $supplement_holdings_add = [];
             }
-        } else { # textual are non additional, but primary
-            $supplement_holdings = $supplement_holdings_add;
-            $supplement_holdings_add = [];
-        }
 
-        if (!exists($skip_computable{'index'})) {
-            foreach my $cap_id ($mfhd->caption_link_ids('855')) {
-                my @indexes = $mfhd->holdings('865', $cap_id);
-                next unless scalar @indexes;
-                foreach (@indexes) {
-                    push @$index_holdings, $_->format();
+            if (!exists($skip_computable{'index'})) {
+                foreach my $cap_id ($mfhd->caption_link_ids('855')) {
+                    my @indexes = $mfhd->holdings('865', $cap_id);
+                    next unless scalar @indexes;
+                    foreach (@indexes) {
+                        push @$index_holdings, $_->format();
+                    }
                 }
-            }
-            if (!@$index_holdings) { # no computed holdings found
+                if (!@$index_holdings) { # no computed holdings found
+                    $index_holdings = $index_holdings_add;
+                    $index_holdings_add = [];
+                }
+            } else { # textual are non additional, but primary
                 $index_holdings = $index_holdings_add;
                 $index_holdings_add = [];
             }
-        } else { # textual are non additional, but primary
-            $index_holdings = $index_holdings_add;
-            $index_holdings_add = [];
         }
 
         # Laurentian extensions
@@ -271,14 +273,14 @@ Given an MFHD record, return a populated svr instance
 =cut
 
 sub generate_svr {
-    my ($self, $id, $mfhd, $owning_lib) = @_;
+    my ($self, $id, $mfhd, $owning_lib, $skip_all_computable) = @_;
 
     if (!$mfhd) {
         return undef;
     }
 
     my $record   = init_holdings_virtual_record();
-    my $holdings = $self->mfhd_to_hash($mfhd);
+    my $holdings = $self->mfhd_to_hash($mfhd, $skip_all_computable);
 
     $record->sre_id($id);
     $record->owning_lib($owning_lib);
index c6e7f4f..9ecaebe 100644 (file)
@@ -206,6 +206,27 @@ sub summary_json {
         $self->fetch_content('AnnotationDetail', $key));
 }
 
+sub available_json {
+    my($self, $key) = @_;
+    my $xml = $self->fetch_content('AvailableContent', $key);
+    my $doc = XML::LibXML->new->parse_string($xml);
+
+    my @avail;
+    for my $node ($doc->findnodes('//*[text()="true"]')) {
+        push(@avail, 'summary') if $node->nodeName eq 'Annotation';
+        push(@avail, 'jacket') if $node->nodeName eq 'Jacket';
+        push(@avail, 'toc') if $node->nodeName eq 'TOC';
+        push(@avail, 'anotes') if $node->nodeName eq 'Biography';
+        push(@avail, 'excerpt') if $node->nodeName eq 'Excerpt';
+        push(@avail, 'reviews') if $node->nodeName eq 'Review';
+    }
+
+    return { 
+        content_type => 'text/plain', 
+        content => OpenSRF::Utils::JSON->perl2JSON(\@avail)
+    };
+}
+
 
 # --------------------------------------------------------------------------
 
index 634fdc7..2be38a0 100644 (file)
@@ -1,6 +1,6 @@
 # ---------------------------------------------------------------
 # Copyright (C) 2009 David Christensen <david.a.christensen@gmail.com>
-# Copyright (C) 2009 Dan Scott <dscott@laurentian.ca>
+# Copyright (C) 2009-2011 Dan Scott <dscott@laurentian.ca>
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
@@ -30,7 +30,13 @@ my $AC = 'OpenILS::WWW::AddedContent';
 
 # These URLs are always the same for OpenLibrary, so there's no advantage to
 # pulling from opensrf.xml; we hardcode them here
-my $base_url = 'http://openlibrary.org/api/books?details=true&bibkeys=ISBN:';
+
+# jscmd=details is unstable but includes goodies such as Table of Contents
+my $base_url_details = 'http://openlibrary.org/api/books?format=json&jscmd=details&bibkeys=ISBN:';
+
+# jscmd=data is stable and contains links to ebooks, excerpts, etc
+my $base_url_data = 'http://openlibrary.org/api/books?format=json&jscmd=data&bibkeys=ISBN:';
+
 my $cover_base_url = 'http://covers.openlibrary.org/b/isbn/';
 
 sub new {
@@ -43,23 +49,120 @@ sub new {
 sub jacket_small {
     my( $self, $key ) = @_;
     return $self->send_img(
-        $self->fetch_cover_response('-S.jpg', $key));
+        $self->fetch_cover_response('small', $key));
 }
 
 sub jacket_medium {
     my( $self, $key ) = @_;
     return $self->send_img(
-        $self->fetch_cover_response('-M.jpg', $key));
+        $self->fetch_cover_response('medium', $key));
 
 }
 sub jacket_large {
     my( $self, $key ) = @_;
     return $self->send_img(
-        $self->fetch_cover_response('-L.jpg', $key));
+        $self->fetch_cover_response('large', $key));
 }
 
 # --------------------------------------------------------------------------
 
+sub ebooks_html {
+    my( $self, $key ) = @_;
+    my $book_data_json = $self->fetch_data_response($key)->content();
+
+    $logger->debug("$key: " . $book_data_json);
+
+    my $ebook_html;
+    
+    my $book_data = OpenSRF::Utils::JSON->JSON2perl($book_data_json);
+    my $book_key = (keys %$book_data)[0];
+
+    # We didn't find a matching book; short-circuit our response
+    if (!$book_key) {
+        $logger->debug("$key: no found book");
+        return 0;
+    }
+
+    my $ebooks_json = $book_data->{$book_key}->{ebooks};
+
+    # No ebooks are available for this book; short-circuit
+    if (!$ebooks_json or !scalar(@$ebooks_json)) {
+        $logger->debug("$key: no ebooks");
+        return 0;
+    }
+
+    # Check the availability of the ebooks
+    my $available = $ebooks_json->[0]->{'availability'} || '';
+    if (!$available) {
+        $logger->debug("$key: no available ebooks");
+        return 0;
+    }
+
+    # Build a basic list of available ebook types and their URLs
+    # ebooks appears to be an array containing one element - a hash
+
+    # First element of the hash is 'read_url' which is a URL to
+    # Internet Archive online reader
+    my $stream_url = $ebooks_json->[0]->{'read_url'} || '';
+    if ($stream_url) {
+        $ebook_html .= "<li class='ebook_stream'><a href='$stream_url'>Read online</a></li>\n";
+        $logger->debug("$key: stream URL = $stream_url");
+    }
+
+    my $ebook_formats = $ebooks_json->[0]->{'formats'} || '';
+    # Next elements are various ebook formats that are available
+    foreach my $ebook (keys %{$ebook_formats}) {
+        if ($ebook_formats->{$ebook} eq 'read_url') {
+            next;
+        }
+        $ebook_html .= "<li class='ebook_$ebook'><a href='" . 
+            $ebook_formats->{$ebook}->{'url'} . "'>" . uc($ebook) . "</a></li>\n";
+    }
+
+    $logger->debug("$key: $ebook_html");
+    $self->send_html("<ul class='ebooks'>$ebook_html</ul>");
+}
+
+sub excerpt_html {
+    my( $self, $key ) = @_;
+    my $book_details_json = $self->fetch_details_response($key)->content();
+
+    $logger->debug("$key: $book_details_json");
+
+    my $excerpt_html;
+    
+    my $book_details = OpenSRF::Utils::JSON->JSON2perl($book_details_json);
+    my $book_key = (keys %$book_details)[0];
+
+    # We didn't find a matching book; short-circuit our response
+    if (!$book_key) {
+        $logger->debug("$key: no found book");
+        return 0;
+    }
+
+    my $first_sentence = $book_details->{$book_key}->{first_sentence};
+    if ($first_sentence) {
+        $excerpt_html .= "<div class='sentence1'>$first_sentence</div>\n";
+    }
+
+    my $excerpts_json = $book_details->{$book_key}->{excerpts};
+    if ($excerpts_json && scalar(@$excerpts_json)) {
+        # Load up excerpt text with comments in tooltip
+        foreach my $excerpt (@$excerpts_json) {
+            my $text = $excerpt->{text};
+            my $cmnt = $excerpt->{comment};
+            $excerpt_html .= "<div class='ac_excerpt' title='$text'>$cmnt</div>\n";
+        }
+    }
+
+    if (!$excerpt_html) {
+        return 0;
+    }
+
+    $logger->debug("$key: $excerpt_html");
+    $self->send_html("<div class='ac_excerpts'>$excerpt_html</div>");
+}
+
 =head1
 
 OpenLibrary returns a JSON hash of zero or more book responses matching our
@@ -74,12 +177,7 @@ HTML table.
 
 sub toc_html {
     my( $self, $key ) = @_;
-    my $book_details_json = $self->fetch_response($key)->content();
-
-
-    # Trim the "var _OlBookInfo = " declaration that makes this
-    # invalid JSON
-    $book_details_json =~ s/^.+?({.*?});$/$1/s;
+    my $book_details_json = $self->fetch_details_response($key)->content();
 
     $logger->debug("$key: " . $book_details_json);
 
@@ -114,9 +212,9 @@ sub toc_html {
         my $page_number = $chapter->{pagenum} || '';
  
         $toc_html .= '<tr>' .
-            "<td style='text-align: right;'>$label</td>" .
-            "<td style='text-align: left; padding-right: 2em;'>$title</td>" .
-            "<td style='text-align: right;'>$page_number</td>" .
+            "<td class='toc_label'>$label</td>" .
+            "<td class='toc_title'>$title</td>" .
+            "<td class='toc_page'>$page_number</td>" .
             "</tr>\n";
     }
 
@@ -127,7 +225,7 @@ sub toc_html {
 sub toc_json {
     my( $self, $key ) = @_;
     my $toc = $self->send_json(
-        $self->fetch_response($key)
+        $self->fetch_details_response($key)
     );
 }
 
@@ -167,9 +265,16 @@ sub send_html {
 }
 
 # returns the HTTP response object from the URL fetch
-sub fetch_response {
+sub fetch_data_response {
     my( $self, $key ) = @_;
-    my $url = $base_url . "$key";
+    my $url = $base_url_data . "$key";
+    my $response = $AC->get_url($url);
+    return $response;
+}
+# returns the HTTP response object from the URL fetch
+sub fetch_details_response {
+    my( $self, $key ) = @_;
+    my $url = $base_url_details . "$key";
     my $response = $AC->get_url($url);
     return $response;
 }
@@ -177,8 +282,26 @@ sub fetch_response {
 # returns the HTTP response object from the URL fetch
 sub fetch_cover_response {
     my( $self, $size, $key ) = @_;
-    my $url = $cover_base_url . "$key$size";
-    return $AC->get_url($url);
+
+    my $response = $self->fetch_data_response($key)->content();
+
+    my $book_data = OpenSRF::Utils::JSON->JSON2perl($response);
+    my $book_key = (keys %$book_data)[0];
+
+    # We didn't find a matching book; short-circuit our response
+    if (!$book_key) {
+        $logger->debug("$key: no found book");
+        return 0;
+    }
+
+    my $covers_json = $book_data->{$book_key}->{cover};
+    if (!$covers_json) {
+        $logger->debug("$key: no covers for this book");
+        return 0;
+    }
+
+    $logger->debug("$key: " . $covers_json->{$size});
+    return $AC->get_url($covers_json->{$size}) || 0;
 }
 
 
index 329935b..d3a6ae8 100644 (file)
@@ -273,7 +273,7 @@ with holdings information.
 The feed type could end with the string "-full", in which case we want
 to return call numbers, copies, and URIS.
 
-Or the feed type could be "-uris", in which case we want to return
+Or the feed type could end with "-uris", in which case we want to return
 call numbers and URIS.
 
 Otherwise, we won't return any holdings.
@@ -281,14 +281,14 @@ Otherwise, we won't return any holdings.
 =cut
 
 sub parse_feed_type {
-    my $type = shift;
+    my $type = shift || '';
 
      if ($type =~ /-full$/o) {
         return 1;
     }
 
      if ($type =~ /-uris$/o) {
-        return "uris";
+        return 2;
     }
 
     # Otherwise, we'll return just the facts, ma'am
@@ -412,21 +412,22 @@ sub unapi {
     # Enable localized results of copy status, etc
     $supercat->session_locale($locale);
 
-    my $format = $cgi->param('format');
+    my $format = $cgi->param('format') || '';
     my $flesh_feed = parse_feed_type($format);
     (my $base_format = $format) =~ s/(-full|-uris)$//o;
-    my ($id,$type,$command,$lib,$depth,$paging) = ('','','');
+    my ($id,$type,$command,$lib,$depth,$paging) = ('','record','');
+    my $body = "Content-type: application/xml; charset=utf-8\n\n";
+
+    if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^\/[]+)(?:\[([0-9,]+)\])?(?:/(.+))?}o) {
+        $id = $2;
+        $paging = $3;
+        ($lib,$depth) = split('/', $4);
+        $type = 'metarecord' if ($1 =~ /^m/o);
+        $type = 'authority' if ($1 =~ /^authority/o);
+    }
 
     if (!$format) {
-        my $body = "Content-type: application/xml; charset=utf-8\n\n";
-    
         if ($uri =~ m{^tag:[^:]+:([^\/]+)/([^\/[]+)(?:\[([0-9,]+)\])?(?:/(.+))?}o) {
-            $id = $2;
-            $paging = $3;
-            ($lib,$depth) = split('/', $4);
-            $type = 'record';
-            $type = 'metarecord' if ($1 =~ /^m/o);
-            $type = 'authority' if ($1 =~ /^authority/o);
 
             my $list = $supercat
                 ->request("open-ils.supercat.$type.formats")
@@ -1480,7 +1481,7 @@ sub create_record_feed {
         next unless $node;
 
         $xml = '';
-        if ($lib && ($type eq 'marcxml' || $type eq 'atom') && ($flesh > 0 || $flesh eq 'uris')) {
+        if ($lib && ($type eq 'marcxml' || $type eq 'atom') && ($flesh > 0)) {
             my $r = $supercat->request( "open-ils.supercat.$search.holdings_xml.retrieve", $rec, $lib, $depth, $flesh_feed, $paging );
             while ( !$r->complete ) {
                 $xml .= join('', map {$_->content} $r->recv);
@@ -1491,8 +1492,8 @@ sub create_record_feed {
 
         $node->id($item_tag);
         #$node->update_ts(cleanse_ISO8601($record->edit_date));
-        $node->link(alternate => $feed->unapi . "?id=$item_tag&format=htmlholdings-full" => 'text/html') if ($flesh > 0 || $flesh eq 'uris');
-        $node->link(opac => $feed->unapi . "?id=$item_tag&format=opac") if ($flesh > 0 || $flesh eq 'uris');
+        $node->link(alternate => $feed->unapi . "?id=$item_tag&format=htmlholdings-full" => 'text/html') if ($flesh > 0);
+        $node->link(opac => $feed->unapi . "?id=$item_tag&format=opac") if ($flesh > 0);
         $node->link(unapi => $feed->unapi . "?id=$item_tag") if ($flesh);
         $node->link('unapi-id' => $item_tag) if ($flesh);
     }
index 6f7f95e..d618782 100755 (executable)
@@ -368,7 +368,7 @@ sub send_success {
        $tmpl =~ s/{TO}/$r->{email}/smog;
        $tmpl =~ s/{FROM}/$email_sender/smog;
        $tmpl =~ s/{REPLY_TO}/$email_sender/smog;
-       $tmpl =~ s/{REPORT_NAME}/$r->{report}->{template}->{name} -- $r->{report}->{name}/smog;
+       $tmpl =~ s/{REPORT_NAME}/$r->{report}->{name} -- $r->{report}->{template}->{name}/smog;
        $tmpl =~ s/{RUN_TIME}/$r->{run_time}/smog;
        $tmpl =~ s/{COMPLETE_TIME}/$r->{complete_time}/smog;
        $tmpl =~ s/{OUTPUT_URL}/$url/smog;
@@ -389,7 +389,7 @@ sub send_fail {
        $tmpl =~ s/{TO}/$r->{email}/smog;
        $tmpl =~ s/{FROM}/$email_sender/smog;
        $tmpl =~ s/{REPLY_TO}/$email_sender/smog;
-       $tmpl =~ s/{REPORT_NAME}/$r->{report}->{template}->{name} -- $r->{report}->{name}/smog;
+       $tmpl =~ s/{REPORT_NAME}/$r->{report}->{name} -- $r->{report}->{template}->{name}/smog;
        $tmpl =~ s/{RUN_TIME}/$r->{run_time}/smog;
        $tmpl =~ s/{ERROR_TEXT}/$r->{error_text}/smog;
        $tmpl =~ s/{SQL}/$sql/smog;
index a9eca28..c97a1dd 100644 (file)
@@ -491,9 +491,12 @@ CREATE OR REPLACE FUNCTION maintain_control_numbers() RETURNS TRIGGER AS $func$
 use strict;
 use MARC::Record;
 use MARC::File::XML (BinaryEncoding => 'UTF-8');
+use MARC::Charset;
 use Encode;
 use Unicode::Normalize;
 
+MARC::Charset->assume_unicode(1);
+
 my $record = MARC::Record->new_from_xml($_TD->{new}{marc});
 my $schema = $_TD->{table_schema};
 my $rec_id = $_TD->{new}{id};
index 5bbb33d..2bcb3c9 100644 (file)
@@ -54,10 +54,39 @@ ALTER TABLE config.global_flag ADD PRIMARY KEY (name);
 
 CREATE TABLE config.upgrade_log (
     version         TEXT    PRIMARY KEY,
-    install_date    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
+    install_date    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+    applied_to      TEXT
 );
 
-INSERT INTO config.upgrade_log (version) VALUES ('0523'); -- dbs
+CREATE TABLE config.db_patch_dependencies (
+  db_patch      TEXT PRIMARY KEY,
+  supersedes    TEXT[],
+  deprecates    TEXT[]
+);
+
+CREATE OR REPLACE FUNCTION evergreen.array_overlap_check (/* field */) RETURNS TRIGGER AS $$
+DECLARE
+    fld     TEXT;
+    cnt     INT;
+BEGIN
+    fld := TG_ARGV[1];
+    EXECUTE 'SELECT COUNT(*) FROM '|| TG_TABLE_SCHEMA ||'.'|| TG_TABLE_NAME ||' WHERE '|| fld ||' && ($1).'|| fld INTO cnt USING NEW;
+    IF cnt > 0 THEN
+        RAISE EXCEPTION 'Cannot insert duplicate array into field % of table %', fld, TG_TABLE_SCHEMA ||'.'|| TG_TABLE_NAME;
+    END IF;
+    RETURN NEW;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE TRIGGER no_overlapping_sups
+    BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
+    FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('supersedes');
+
+CREATE TRIGGER no_overlapping_deps
+    BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
+    FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
+
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0533', :eg_version); -- gmc
 
 CREATE TABLE config.bib_source (
        id              SERIAL  PRIMARY KEY,
@@ -794,4 +823,68 @@ BEGIN
 END;
 $$ LANGUAGE PLPGSQL;
 
+-- List applied db patches that are deprecated by (and block the application of) my_db_patch
+CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_deprecates ( my_db_patch TEXT ) RETURNS SETOF TEXT AS $$
+    SELECT  DISTINCT l.version
+      FROM  config.upgrade_log l
+            JOIN config.db_patch_dependencies d ON (l.version::TEXT[] && d.deprecates)
+      WHERE d.db_patch = $1 
+$$ LANGUAGE SQL;
+
+-- List applied db patches that are superseded by (and block the application of) my_db_patch
+CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_supersedes ( my_db_patch TEXT ) RETURNS SETOF TEXT AS $$
+    SELECT  DISTINCT l.version
+      FROM  config.upgrade_log l
+            JOIN config.db_patch_dependencies d ON (l.version::TEXT[] && d.supersedes)
+      WHERE d.db_patch = $1 
+$$ LANGUAGE SQL;
+
+-- List applied db patches that deprecates (and block the application of) my_db_patch
+CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_deprecated ( my_db_patch TEXT ) RETURNS TEXT AS $$
+    SELECT  db_patch
+      FROM  config.db_patch_dependencies
+      WHERE ARRAY[$1]::TEXT[] && deprecates 
+$$ LANGUAGE SQL;
+
+-- List applied db patches that supersedes (and block the application of) my_db_patch
+CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_superseded ( my_db_patch TEXT ) RETURNS TEXT AS $$
+    SELECT  db_patch
+      FROM  config.db_patch_dependencies
+      WHERE ARRAY[$1]::TEXT[] && supersedes 
+$$ LANGUAGE SQL;
+
+-- Make sure that no deprecated or superseded db patches are currently applied
+CREATE OR REPLACE FUNCTION evergreen.upgrade_verify_no_dep_conflicts ( my_db_patch TEXT ) RETURNS BOOL AS $$
+    SELECT  COUNT(*) = 0
+      FROM  (SELECT * FROM evergreen.upgrade_list_applied_deprecates( $1 )
+                UNION
+             SELECT * FROM evergreen.upgrade_list_applied_supersedes( $1 )
+                UNION
+             SELECT * FROM evergreen.upgrade_list_applied_deprecated( $1 )
+                UNION
+             SELECT * FROM evergreen.upgrade_list_applied_superseded( $1 ))x
+$$ LANGUAGE SQL;
+
+-- Raise an exception if there are, in fact, dep/sup confilct
+CREATE OR REPLACE FUNCTION evergreen.upgrade_deps_block_check ( my_db_patch TEXT, my_applied_to TEXT ) RETURNS BOOL AS $$
+BEGIN
+    IF NOT evergreen.upgrade_verify_no_dep_conflicts( my_db_patch ) THEN
+        RAISE EXCEPTION '
+Upgrade script % can not be applied:
+  applied deprecated scripts %
+  applied superseded scripts %
+  deprecated by %
+  superseded by %',
+            my_db_patch,
+            ARRAY_ACUM(evergreen.upgrade_list_applied_deprecates(my_db_patch)),
+            ARRAY_ACUM(evergreen.upgrade_list_applied_supersedes(my_db_patch)),
+            evergreen.upgrade_list_applied_deprecated(my_db_patch),
+            evergreen.upgrade_list_applied_superseded(my_db_patch);
+    END IF;
+
+    INSERT INTO config.upgrade_log (version, applied_to) VALUES (my_db_patch, my_applied_to);
+    RETURN TRUE;
+END;
+$$ LANGUAGE PLPGSQL;
+
 COMMIT;
index f87ffa0..d56e644 100644 (file)
@@ -88,6 +88,15 @@ CREATE INDEX actor_usr_day_phone_idx ON actor.usr (evergreen.lowercase(day_phone
 CREATE INDEX actor_usr_evening_phone_idx ON actor.usr (evergreen.lowercase(evening_phone));
 CREATE INDEX actor_usr_other_phone_idx ON actor.usr (evergreen.lowercase(other_phone));
 
+CREATE INDEX actor_usr_day_phone_idx_numeric ON actor.usr USING BTREE
+    (evergreen.lowercase(REGEXP_REPLACE(day_phone, '[^0-9]', '', 'g')));
+
+CREATE INDEX actor_usr_evening_phone_idx_numeric ON actor.usr USING BTREE
+    (evergreen.lowercase(REGEXP_REPLACE(evening_phone, '[^0-9]', '', 'g')));
+
+CREATE INDEX actor_usr_other_phone_idx_numeric ON actor.usr USING BTREE
+    (evergreen.lowercase(REGEXP_REPLACE(other_phone, '[^0-9]', '', 'g')));
+
 CREATE INDEX actor_usr_ident_value_idx ON actor.usr (evergreen.lowercase(ident_value));
 CREATE INDEX actor_usr_ident_value2_idx ON actor.usr (evergreen.lowercase(ident_value2));
 
index 3f3f3e5..133a243 100644 (file)
@@ -121,6 +121,9 @@ CREATE OR REPLACE FUNCTION authority.generate_overlay_template ( TEXT, BIGINT )
 
     use MARC::Record;
     use MARC::File::XML (BinaryEncoding => 'UTF-8');
+    use MARC::Charset;
+
+    MARC::Charset->assume_unicode(1);
 
     my $xml = shift;
     my $r = MARC::Record->new_from_xml( $xml );
index 476ee07..7841170 100644 (file)
@@ -309,8 +309,11 @@ CREATE OR REPLACE FUNCTION vandelay.add_field ( target_xml TEXT, source_xml TEXT
 
     use MARC::Record;
     use MARC::File::XML (BinaryEncoding => 'UTF-8');
+    use MARC::Charset;
     use strict;
 
+    MARC::Charset->assume_unicode(1);
+
     my $target_xml = shift;
     my $source_xml = shift;
     my $field_spec = shift;
@@ -386,8 +389,11 @@ CREATE OR REPLACE FUNCTION vandelay.strip_field ( xml TEXT, field TEXT ) RETURNS
 
     use MARC::Record;
     use MARC::File::XML (BinaryEncoding => 'UTF-8');
+    use MARC::Charset;
     use strict;
 
+    MARC::Charset->assume_unicode(1);
+
     my $xml = shift;
     my $r = MARC::Record->new_from_xml( $xml );
 
index f7c43c5..981a364 100644 (file)
@@ -338,8 +338,11 @@ CREATE OR REPLACE FUNCTION authority.normalize_heading( TEXT ) RETURNS TEXT AS $
     use utf8;
     use MARC::Record;
     use MARC::File::XML (BinaryEncoding => 'UTF8');
+    use MARC::Charset;
     use UUID::Tiny ':std';
 
+    MARC::Charset->assume_unicode(1);
+
     my $xml = shift() or return undef;
 
     my $r;
index 9e8272f..5dc4761 100644 (file)
@@ -430,6 +430,9 @@ CREATE OR REPLACE FUNCTION biblio.flatten_marc ( TEXT ) RETURNS SETOF metabib.fu
 
 use MARC::Record;
 use MARC::File::XML (BinaryEncoding => 'UTF-8');
+use MARC::Charset;
+
+MARC::Charset->assume_unicode(1);
 
 my $xml = shift;
 my $r = MARC::Record->new_from_xml( $xml );
@@ -1065,9 +1068,10 @@ BEGIN
                     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
+                    SELECT  m.value INTO attr_value
+                      FROM  biblio.marc21_physical_characteristics(NEW.id) v
+                            JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
+                      WHERE v.subfield = attr_def.phys_char_sf
                       LIMIT 1; -- Just in case ...
 
                 END IF;
index 591543f..3e46a19 100644 (file)
@@ -433,7 +433,7 @@ BEGIN
           FROM  
                 actor.org_unit_descendants(ans.id) d
                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
-                JOIN asset.copy cp ON (cp.id = av.id)
+                JOIN asset.copy cp ON (cp.id = av.copy_id)
           GROUP BY 1,2,6;
 
         IF NOT FOUND THEN
@@ -464,7 +464,7 @@ BEGIN
           FROM
                 actor.org_unit_descendants(ans.id) d
                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
-                JOIN asset.copy cp ON (cp.id = av.id)
+                JOIN asset.copy cp ON (cp.id = av.copy_id)
           GROUP BY 1,2,6;
 
         IF NOT FOUND THEN
@@ -577,7 +577,7 @@ BEGIN
           FROM  
                 actor.org_unit_descendants(ans.id) d
                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
-                JOIN asset.copy cp ON (cp.id = av.id)
+                JOIN asset.copy cp ON (cp.id = av.copy_id)
                 JOIN metabib.metarecord_source_map m ON (m.source = av.record)
           GROUP BY 1,2,6;
 
@@ -609,7 +609,7 @@ BEGIN
           FROM
                 actor.org_unit_descendants(ans.id) d
                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
-                JOIN asset.copy cp ON (cp.id = av.id)
+                JOIN asset.copy cp ON (cp.id = av.copy_id)
                 JOIN metabib.metarecord_source_map m ON (m.source = av.record)
           GROUP BY 1,2,6;
 
index 521171c..febf569 100644 (file)
@@ -10,6 +10,7 @@ CREATE TABLE config.circ_matrix_weights (
     circ_modifier           NUMERIC(6,2)   NOT NULL,
     marc_type               NUMERIC(6,2)   NOT NULL,
     marc_form               NUMERIC(6,2)   NOT NULL,
+    marc_bib_level          NUMERIC(6,2)   NOT NULL,
     marc_vr_format          NUMERIC(6,2)   NOT NULL,
     copy_circ_lib           NUMERIC(6,2)   NOT NULL,
     copy_owning_lib         NUMERIC(6,2)   NOT NULL,
@@ -35,6 +36,7 @@ CREATE TABLE config.hold_matrix_weights (
     circ_modifier           NUMERIC(6,2)   NOT NULL,
     marc_type               NUMERIC(6,2)   NOT NULL,
     marc_form               NUMERIC(6,2)   NOT NULL,
+    marc_bib_level          NUMERIC(6,2)   NOT NULL,
     marc_vr_format          NUMERIC(6,2)   NOT NULL,
     juvenile_flag           NUMERIC(6,2)   NOT NULL,
     ref_flag                NUMERIC(6,2)   NOT NULL
index ad5376d..28c45ba 100644 (file)
@@ -110,6 +110,9 @@ INSERT INTO container.biblio_record_entry_bucket_type (code,label) VALUES ('temp
 -- under a different name:
 
 ALTER TABLE booking.resource_type
+       ALTER COLUMN record TYPE BIGINT;
+
+ALTER TABLE booking.resource_type
        ADD CONSTRAINT brt_name_and_record_once_per_owner UNIQUE(owner, name, record);
 
 -- Now upgrade permission.perm_list.  This is fairly complicated.
@@ -3167,37 +3170,77 @@ INSERT INTO action_trigger.validator (module,description) VALUES (
     )
 ;
 
--- What was event_definition #15 in v1.6.1 will be recreated as #20.  This
+
+-- The password reset event_definition in v1.6.1 will be moved to #20.  This
 -- renumbering requires some juggling:
 --
 -- 1. Update any child rows to point to #20.  These updates will temporarily
--- violate foreign key constraints, but that's okay as long as we create
+-- violate foreign key constraints, but that's okay as long as we have a
 -- #20 before committing.
 --
--- 2. Delete the old #15.
---
--- 3. Insert the new #15.
+-- 2. Update the id of the password reset event_definition to 20
 --
--- 4. Insert #20.
---
--- We could combine steps 2 and 3 into a single update, but that would create
--- additional opportunities for typos, since we already have the insert from
--- an upgrade script.
+-- This code might fail in some cases, but should work with all stock 1.6.1
+-- instances, whether fresh or via upgrade
 
 UPDATE action_trigger.environment
 SET event_def = 20
-WHERE event_def = 15;
+WHERE event_def = (SELECT id FROM action_trigger.event_definition WHERE hook = 'password.reset_request' ORDER BY id ASC LIMIT 1);
 
 UPDATE action_trigger.event
 SET event_def = 20
-WHERE event_def = 15;
+WHERE event_def = (SELECT id FROM action_trigger.event_definition WHERE hook = 'password.reset_request' ORDER BY id ASC LIMIT 1);
 
 UPDATE action_trigger.event_params
 SET event_def = 20
-WHERE event_def = 15;
+WHERE event_def = (SELECT id FROM action_trigger.event_definition WHERE hook = 'password.reset_request' ORDER BY id ASC LIMIT 1);
+
+UPDATE action_trigger.event_definition
+SET id = 20
+WHERE id = (SELECT id FROM action_trigger.event_definition WHERE hook = 'password.reset_request' ORDER BY id ASC LIMIT 1);
+
+
+-- Let's also take the opportunity to rebuild the trigger
+-- if it got mangled somehow
+INSERT INTO action_trigger.hook (key,core_type,description)
+ SELECT  'password.reset_request','aupr','Patron has requested a self-serve password reset'
+   WHERE (SELECT COUNT(*) FROM action_trigger.hook WHERE key = 'password.reset_request') = 0;
+
+INSERT INTO action_trigger.event_definition (id, active, owner, name, hook, validator, reactor, delay, template)
+    SELECT 20, 'f', 1, 'Password reset request notification', 'password.reset_request', 'NOOP_True', 'SendEmail', '00:00:01',
+$$
+[%- USE date -%]
+[%- user = target.usr -%]
+To: [%- params.recipient_email || user.email %]
+From: [%- params.sender_email || user.home_ou.email || default_sender %]
+Subject: [% user.home_ou.name %]: library account password reset request
+
+You have received this message because you, or somebody else, requested a reset
+of your library system password. If you did not request a reset of your library
+system password, just ignore this message and your current password will
+continue to work.
+
+If you did request a reset of your library system password, please perform
+the following steps to continue the process of resetting your password:
 
-DELETE FROM action_trigger.event_definition
-WHERE id = 15;
+1. Open the following link in a web browser: https://[% params.hostname %]/opac/password/[% params.locale || 'en-US' %]/[% target.uuid %]
+The browser displays a password reset form.
+
+2. Enter your new password in the password reset form in the browser. You must
+enter the password twice to ensure that you do not make a mistake. If the
+passwords match, you will then be able to log in to your library system account
+with the new password.
+
+$$
+   WHERE (SELECT COUNT(*) FROM action_trigger.event_definition WHERE id = 20) = 0;
+
+INSERT INTO action_trigger.environment ( event_def, path)
+    SELECT 20, 'usr'
+               WHERE (SELECT COUNT(*) FROM action_trigger.environment WHERE event_def = 20 AND path = 'usr') = 0;
+
+INSERT INTO action_trigger.environment ( event_def, path)
+    SELECT 20, 'usr.home_ou'
+               WHERE (SELECT COUNT(*) FROM action_trigger.environment WHERE event_def = 20 AND path = 'usr.home_ou') = 0;
 
 INSERT INTO action_trigger.event_definition (
         id,
@@ -8355,138 +8398,6 @@ ALTER TABLE action.transit_copy
 ADD COLUMN prev_dest INTEGER REFERENCES actor.org_unit( id )
                                                         DEFERRABLE INITIALLY DEFERRED;
 
-DROP SCHEMA IF EXISTS booking CASCADE;
-
-CREATE SCHEMA booking;
-
-CREATE TABLE booking.resource_type (
-       id             SERIAL          PRIMARY KEY,
-       name           TEXT            NOT NULL,
-       fine_interval  INTERVAL,
-       fine_amount    DECIMAL(8,2)    NOT NULL DEFAULT 0,
-       owner          INT             NOT NULL
-                                      REFERENCES actor.org_unit( id )
-                                      DEFERRABLE INITIALLY DEFERRED,
-       catalog_item   BOOLEAN         NOT NULL DEFAULT FALSE,
-       transferable   BOOLEAN         NOT NULL DEFAULT FALSE,
-    record         BIGINT          REFERENCES biblio.record_entry (id)
-                                      DEFERRABLE INITIALLY DEFERRED,
-    max_fine       NUMERIC(8,2),
-    elbow_room     INTERVAL,
-    CONSTRAINT brt_name_and_record_once_per_owner UNIQUE(owner, name, record)
-);
-
-CREATE TABLE booking.resource (
-       id             SERIAL           PRIMARY KEY,
-       owner          INT              NOT NULL
-                                       REFERENCES actor.org_unit(id)
-                                       DEFERRABLE INITIALLY DEFERRED,
-       type           INT              NOT NULL
-                                       REFERENCES booking.resource_type(id)
-                                       DEFERRABLE INITIALLY DEFERRED,
-       overbook       BOOLEAN          NOT NULL DEFAULT FALSE,
-       barcode        TEXT             NOT NULL,
-       deposit        BOOLEAN          NOT NULL DEFAULT FALSE,
-       deposit_amount DECIMAL(8,2)     NOT NULL DEFAULT 0.00,
-       user_fee       DECIMAL(8,2)     NOT NULL DEFAULT 0.00,
-       CONSTRAINT br_unique UNIQUE (owner, barcode)
-);
-
--- For non-catalog items: hijack barcode for name/description
-
-CREATE TABLE booking.resource_attr (
-       id              SERIAL          PRIMARY KEY,
-       owner           INT             NOT NULL
-                                       REFERENCES actor.org_unit(id)
-                                       DEFERRABLE INITIALLY DEFERRED,
-       name            TEXT            NOT NULL,
-       resource_type   INT             NOT NULL
-                                       REFERENCES booking.resource_type(id)
-                                       ON DELETE CASCADE
-                                       DEFERRABLE INITIALLY DEFERRED,
-       required        BOOLEAN         NOT NULL DEFAULT FALSE,
-       CONSTRAINT bra_name_once_per_type UNIQUE(resource_type, name)
-);
-
-CREATE TABLE booking.resource_attr_value (
-       id               SERIAL         PRIMARY KEY,
-       owner            INT            NOT NULL
-                                       REFERENCES actor.org_unit(id)
-                                       DEFERRABLE INITIALLY DEFERRED,
-       attr             INT            NOT NULL
-                                       REFERENCES booking.resource_attr(id)
-                                       DEFERRABLE INITIALLY DEFERRED,
-       valid_value      TEXT           NOT NULL,
-       CONSTRAINT brav_logical_key UNIQUE(owner, attr, valid_value)
-);
-
-CREATE TABLE booking.resource_attr_map (
-       id               SERIAL         PRIMARY KEY,
-       resource         INT            NOT NULL
-                                       REFERENCES booking.resource(id)
-                                       ON DELETE CASCADE
-                                       DEFERRABLE INITIALLY DEFERRED,
-       resource_attr    INT            NOT NULL
-                                       REFERENCES booking.resource_attr(id)
-                                       ON DELETE CASCADE
-                                       DEFERRABLE INITIALLY DEFERRED,
-       value            INT            NOT NULL
-                                       REFERENCES booking.resource_attr_value(id)
-                                       DEFERRABLE INITIALLY DEFERRED,
-       CONSTRAINT bram_one_value_per_attr UNIQUE(resource, resource_attr)
-);
-
-CREATE TABLE booking.reservation (
-       request_time     TIMESTAMPTZ   NOT NULL DEFAULT now(),
-       start_time       TIMESTAMPTZ,
-       end_time         TIMESTAMPTZ,
-       capture_time     TIMESTAMPTZ,
-       cancel_time      TIMESTAMPTZ,
-       pickup_time      TIMESTAMPTZ,
-       return_time      TIMESTAMPTZ,
-       booking_interval INTERVAL,
-       fine_interval    INTERVAL,
-       fine_amount      DECIMAL(8,2),
-       target_resource_type  INT       NOT NULL
-                                       REFERENCES booking.resource_type(id)
-                                       ON DELETE CASCADE
-                                       DEFERRABLE INITIALLY DEFERRED,
-       target_resource  INT            REFERENCES booking.resource(id)
-                                       ON DELETE CASCADE
-                                       DEFERRABLE INITIALLY DEFERRED,
-       current_resource INT            REFERENCES booking.resource(id)
-                                       ON DELETE CASCADE
-                                       DEFERRABLE INITIALLY DEFERRED,
-       request_lib      INT            NOT NULL
-                                       REFERENCES actor.org_unit(id)
-                                       DEFERRABLE INITIALLY DEFERRED,
-       pickup_lib       INT            REFERENCES actor.org_unit(id)
-                                       DEFERRABLE INITIALLY DEFERRED,
-       capture_staff    INT            REFERENCES actor.usr(id)
-                                       DEFERRABLE INITIALLY DEFERRED,
-    max_fine         NUMERIC(8,2)
-) INHERITS (money.billable_xact);
-
-ALTER TABLE booking.reservation ADD PRIMARY KEY (id);
-
-ALTER TABLE booking.reservation
-       ADD CONSTRAINT booking_reservation_usr_fkey
-       FOREIGN KEY (usr) REFERENCES actor.usr (id)
-       DEFERRABLE INITIALLY DEFERRED;
-
-CREATE TABLE booking.reservation_attr_value_map (
-       id               SERIAL         PRIMARY KEY,
-       reservation      INT            NOT NULL
-                                       REFERENCES booking.reservation(id)
-                                       ON DELETE CASCADE
-                                       DEFERRABLE INITIALLY DEFERRED,
-       attr_value       INT            NOT NULL
-                                       REFERENCES booking.resource_attr_value(id)
-                                       ON DELETE CASCADE
-                                       DEFERRABLE INITIALLY DEFERRED,
-       CONSTRAINT bravm_logical_key UNIQUE(reservation, attr_value)
-);
-
 -- represents a circ chain summary
 CREATE TYPE action.circ_chain_summary AS (
     num_circs INTEGER,
@@ -8581,8 +8492,11 @@ BEGIN
 END;
 $$ LANGUAGE 'plpgsql';
 
+DROP TRIGGER IF EXISTS mat_summary_create_tgr ON booking.reservation;
 CREATE TRIGGER mat_summary_create_tgr AFTER INSERT ON booking.reservation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_create ('reservation');
+DROP TRIGGER IF EXISTS mat_summary_change_tgr ON booking.reservation;
 CREATE TRIGGER mat_summary_change_tgr AFTER UPDATE ON booking.reservation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_update ();
+DROP TRIGGER IF EXISTS mat_summary_remove_tgr ON booking.reservation;
 CREATE TRIGGER mat_summary_remove_tgr AFTER DELETE ON booking.reservation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_delete ();
 
 ALTER TABLE config.standing_penalty
@@ -18705,8 +18619,11 @@ CREATE OR REPLACE FUNCTION authority.normalize_heading( TEXT ) RETURNS TEXT AS $
     use utf8;
     use MARC::Record;
     use MARC::File::XML (BinaryEncoding => 'UTF8');
+    use MARC::Charset;
     use UUID::Tiny ':std';
 
+    MARC::Charset->assume_unicode(1);
+
     my $xml = shift() or return undef;
 
     my $r;
@@ -18807,16 +18724,6 @@ UPDATE action.reservation_transit_copy
 -- Recreate some foreign keys that were somehow dropped, probably
 -- by some kind of cascade from an inherited table:
 
-ALTER TABLE action.reservation_transit_copy
-       ADD CONSTRAINT artc_tc_fkey FOREIGN KEY (target_copy)
-               REFERENCES booking.resource(id)
-               ON DELETE CASCADE
-               DEFERRABLE INITIALLY DEFERRED,
-       ADD CONSTRAINT reservation_transit_copy_reservation_fkey FOREIGN KEY (reservation)
-               REFERENCES booking.reservation(id)
-               ON DELETE SET NULL
-               DEFERRABLE INITIALLY DEFERRED;
-
 CREATE INDEX user_bucket_item_target_user_idx
        ON container.user_bucket_item ( target_user );
 
@@ -19067,6 +18974,17 @@ COMMIT;
 -- Some operations go outside of the transaction, because they may
 -- legitimately fail.
 
+ALTER TABLE action.reservation_transit_copy
+       ADD CONSTRAINT artc_tc_fkey FOREIGN KEY (target_copy)
+               REFERENCES booking.resource(id)
+               ON DELETE CASCADE
+               DEFERRABLE INITIALLY DEFERRED,
+       ADD CONSTRAINT reservation_transit_copy_reservation_fkey FOREIGN KEY (reservation)
+               REFERENCES booking.reservation(id)
+               ON DELETE SET NULL
+               DEFERRABLE INITIALLY DEFERRED;
+
+
 \qecho ALTERs of auditor.action_hold_request_history will fail if the table
 \qecho doesn't exist; ignore those errors if they occur.
 
@@ -19193,6 +19111,419 @@ CREATE INDEX cp_create_date  ON asset.copy (create_date);
 -- Speed up call number browsing
 CREATE INDEX asset_call_number_label_sortkey_browse ON asset.call_number(oils_text_as_bytea(label_sortkey), oils_text_as_bytea(label), id, owning_lib) WHERE deleted IS FALSE OR deleted = FALSE;
 
+-- Add MARC::Charset->assume_unicode(1) to improve handling of Unicode characters
+CREATE OR REPLACE FUNCTION maintain_control_numbers() RETURNS TRIGGER AS $func$
+use strict;
+use MARC::Record;
+use MARC::File::XML (BinaryEncoding => 'UTF-8');
+use MARC::Charset;
+use Encode;
+use Unicode::Normalize;
+
+MARC::Charset->assume_unicode(1);
+
+my $record = MARC::Record->new_from_xml($_TD->{new}{marc});
+my $schema = $_TD->{table_schema};
+my $rec_id = $_TD->{new}{id};
+
+# Short-circuit if maintaining control numbers per MARC21 spec is not enabled
+my $enable = spi_exec_query("SELECT enabled FROM config.global_flag WHERE name = 'cat.maintain_control_numbers'");
+if (!($enable->{processed}) or $enable->{rows}[0]->{enabled} eq 'f') {
+    return;
+}
+
+# Get the control number identifier from an OU setting based on $_TD->{new}{owner}
+my $ou_cni = 'EVRGRN';
+
+my $owner;
+if ($schema eq 'serial') {
+    $owner = $_TD->{new}{owning_lib};
+} else {
+    # are.owner and bre.owner can be null, so fall back to the consortial setting
+    $owner = $_TD->{new}{owner} || 1;
+}
+
+my $ous_rv = spi_exec_query("SELECT value FROM actor.org_unit_ancestor_setting('cat.marc_control_number_identifier', $owner)");
+if ($ous_rv->{processed}) {
+    $ou_cni = $ous_rv->{rows}[0]->{value};
+    $ou_cni =~ s/"//g; # Stupid VIM syntax highlighting"
+} else {
+    # Fall back to the shortname of the OU if there was no OU setting
+    $ous_rv = spi_exec_query("SELECT shortname FROM actor.org_unit WHERE id = $owner");
+    if ($ous_rv->{processed}) {
+        $ou_cni = $ous_rv->{rows}[0]->{shortname};
+    }
+}
+
+my ($create, $munge) = (0, 0);
+
+my @scns = $record->field('035');
+
+foreach my $id_field ('001', '003') {
+    my $spec_value;
+    my @controls = $record->field($id_field);
+
+    if ($id_field eq '001') {
+        $spec_value = $rec_id;
+    } else {
+        $spec_value = $ou_cni;
+    }
+
+    # Create the 001/003 if none exist
+    if (scalar(@controls) == 1) {
+        # Only one field; check to see if we need to munge it
+        unless (grep $_->data() eq $spec_value, @controls) {
+            $munge = 1;
+        }
+    } else {
+        # Delete the other fields, as with more than 1 001/003 we do not know which 003/001 to match
+        foreach my $control (@controls) {
+            unless ($control->data() eq $spec_value) {
+                $record->delete_field($control);
+            }
+        }
+        $record->insert_fields_ordered(MARC::Field->new($id_field, $spec_value));
+        $create = 1;
+    }
+}
+
+# Now, if we need to munge the 001, we will first push the existing 001/003
+# into the 035; but if the record did not have one (and one only) 001 and 003
+# to begin with, skip this process
+if ($munge and not $create) {
+    my $scn = "(" . $record->field('003')->data() . ")" . $record->field('001')->data();
+
+    # Do not create duplicate 035 fields
+    unless (grep $_->subfield('a') eq $scn, @scns) {
+        $record->insert_fields_ordered(MARC::Field->new('035', '', '', 'a' => $scn));
+    }
+}
+
+# Set the 001/003 and update the MARC
+if ($create or $munge) {
+    $record->field('001')->data($rec_id);
+    $record->field('003')->data($ou_cni);
+
+    my $xml = $record->as_xml_record();
+    $xml =~ s/\n//sgo;
+    $xml =~ s/^<\?xml.+\?\s*>//go;
+    $xml =~ s/>\s+</></go;
+    $xml =~ s/\p{Cc}//go;
+
+    # Embed a version of OpenILS::Application::AppUtils->entityize()
+    # to avoid having to set PERL5LIB for PostgreSQL as well
+
+    # If we are going to convert non-ASCII characters to XML entities,
+    # we had better be dealing with a UTF8 string to begin with
+    $xml = decode_utf8($xml);
+
+    $xml = NFC($xml);
+
+    # Convert raw ampersands to entities
+    $xml =~ s/&(?!\S+;)/&amp;/gso;
+
+    # Convert Unicode characters to entities
+    $xml =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
+
+    $xml =~ s/[\x00-\x1f]//go;
+    $_TD->{new}{marc} = $xml;
+
+    return "MODIFY";
+}
+
+return;
+$func$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION authority.generate_overlay_template ( TEXT, BIGINT ) RETURNS TEXT AS $func$
+
+    use MARC::Record;
+    use MARC::File::XML (BinaryEncoding => 'UTF-8');
+    use MARC::Charset;
+
+    MARC::Charset->assume_unicode(1);
+
+    my $xml = shift;
+    my $r = MARC::Record->new_from_xml( $xml );
+
+    return undef unless ($r);
+
+    my $id = shift() || $r->subfield( '901' => 'c' );
+    $id =~ s/^\s*(?:\([^)]+\))?\s*(.+)\s*?$/$1/;
+    return undef unless ($id); # We need an ID!
+
+    my $tmpl = MARC::Record->new();
+    $tmpl->encoding( 'UTF-8' );
+
+    my @rule_fields;
+    for my $field ( $r->field( '1..' ) ) { # Get main entry fields from the authority record
+
+        my $tag = $field->tag;
+        my $i1 = $field->indicator(1);
+        my $i2 = $field->indicator(2);
+        my $sf = join '', map { $_->[0] } $field->subfields;
+        my @data = map { @$_ } $field->subfields;
+
+        my @replace_them;
+
+        # Map the authority field to bib fields it can control.
+        if ($tag >= 100 and $tag <= 111) {       # names
+            @replace_them = map { $tag + $_ } (0, 300, 500, 600, 700);
+        } elsif ($tag eq '130') {                # uniform title
+            @replace_them = qw/130 240 440 730 830/;
+        } elsif ($tag >= 150 and $tag <= 155) {  # subjects
+            @replace_them = ($tag + 500);
+        } elsif ($tag >= 180 and $tag <= 185) {  # floating subdivisions
+            @replace_them = qw/100 400 600 700 800 110 410 610 710 810 111 411 611 711 811 130 240 440 730 830 650 651 655/;
+        } else {
+            next;
+        }
+
+        # Dummy up the bib-side data
+        $tmpl->append_fields(
+            map {
+                MARC::Field->new( $_, $i1, $i2, @data )
+            } @replace_them
+        );
+
+        # Construct some 'replace' rules
+        push @rule_fields, map { $_ . $sf . '[0~\)' .$id . '$]' } @replace_them;
+    }
+
+    # Insert the replace rules into the template
+    $tmpl->append_fields(
+        MARC::Field->new( '905' => ' ' => ' ' => 'r' => join(',', @rule_fields ) )
+    );
+
+    $xml = $tmpl->as_xml_record;
+    $xml =~ s/^<\?.+?\?>$//mo;
+    $xml =~ s/\n//sgo;
+    $xml =~ s/>\s+</></sgo;
+
+    return $xml;
+
+$func$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION vandelay.add_field ( target_xml TEXT, source_xml TEXT, field TEXT, force_add INT ) RETURNS TEXT AS $_$
+
+    use MARC::Record;
+    use MARC::File::XML (BinaryEncoding => 'UTF-8');
+    use MARC::Charset;
+    use strict;
+
+    MARC::Charset->assume_unicode(1);
+
+    my $target_xml = shift;
+    my $source_xml = shift;
+    my $field_spec = shift;
+    my $force_add = shift || 0;
+
+    my $target_r = MARC::Record->new_from_xml( $target_xml );
+    my $source_r = MARC::Record->new_from_xml( $source_xml );
+
+    return $target_xml unless ($target_r && $source_r);
+
+    my @field_list = split(',', $field_spec);
+
+    my %fields;
+    for my $f (@field_list) {
+        $f =~ s/^\s*//; $f =~ s/\s*$//;
+        if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
+            my $field = $1;
+            $field =~ s/\s+//;
+            my $sf = $2;
+            $sf =~ s/\s+//;
+            my $match = $3;
+            $match =~ s/^\s*//; $match =~ s/\s*$//;
+            $fields{$field} = { sf => [ split('', $sf) ] };
+            if ($match) {
+                my ($msf,$mre) = split('~', $match);
+                if (length($msf) > 0 and length($mre) > 0) {
+                    $msf =~ s/^\s*//; $msf =~ s/\s*$//;
+                    $mre =~ s/^\s*//; $mre =~ s/\s*$//;
+                    $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
+                }
+            }
+        }
+    }
+
+    for my $f ( keys %fields) {
+        if ( @{$fields{$f}{sf}} ) {
+            for my $from_field ($source_r->field( $f )) {
+                my @tos = $target_r->field( $f );
+                if (!@tos) {
+                    next if (exists($fields{$f}{match}) and !$force_add);
+                    my @new_fields = map { $_->clone } $source_r->field( $f );
+                    $target_r->insert_fields_ordered( @new_fields );
+                } else {
+                    for my $to_field (@tos) {
+                        if (exists($fields{$f}{match})) {
+                            next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
+                        }
+                        my @new_sf = map { ($_ => $from_field->subfield($_)) } @{$fields{$f}{sf}};
+                        $to_field->add_subfields( @new_sf );
+                    }
+                }
+            }
+        } else {
+            my @new_fields = map { $_->clone } $source_r->field( $f );
+            $target_r->insert_fields_ordered( @new_fields );
+        }
+    }
+
+    $target_xml = $target_r->as_xml_record;
+    $target_xml =~ s/^<\?.+?\?>$//mo;
+    $target_xml =~ s/\n//sgo;
+    $target_xml =~ s/>\s+</></sgo;
+
+    return $target_xml;
+
+$_$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION vandelay.strip_field ( xml TEXT, field TEXT ) RETURNS TEXT AS $_$
+
+    use MARC::Record;
+    use MARC::File::XML (BinaryEncoding => 'UTF-8');
+    use MARC::Charset;
+    use strict;
+
+    MARC::Charset->assume_unicode(1);
+
+    my $xml = shift;
+    my $r = MARC::Record->new_from_xml( $xml );
+
+    return $xml unless ($r);
+
+    my $field_spec = shift;
+    my @field_list = split(',', $field_spec);
+
+    my %fields;
+    for my $f (@field_list) {
+        $f =~ s/^\s*//; $f =~ s/\s*$//;
+        if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
+            my $field = $1;
+            $field =~ s/\s+//;
+            my $sf = $2;
+            $sf =~ s/\s+//;
+            my $match = $3;
+            $match =~ s/^\s*//; $match =~ s/\s*$//;
+            $fields{$field} = { sf => [ split('', $sf) ] };
+            if ($match) {
+                my ($msf,$mre) = split('~', $match);
+                if (length($msf) > 0 and length($mre) > 0) {
+                    $msf =~ s/^\s*//; $msf =~ s/\s*$//;
+                    $mre =~ s/^\s*//; $mre =~ s/\s*$//;
+                    $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
+                }
+            }
+        }
+    }
+
+    for my $f ( keys %fields) {
+        for my $to_field ($r->field( $f )) {
+            if (exists($fields{$f}{match})) {
+                next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
+            }
+
+            if ( @{$fields{$f}{sf}} ) {
+                $to_field->delete_subfield(code => $fields{$f}{sf});
+            } else {
+                $r->delete_field( $to_field );
+            }
+        }
+    }
+
+    $xml = $r->as_xml_record;
+    $xml =~ s/^<\?.+?\?>$//mo;
+    $xml =~ s/\n//sgo;
+    $xml =~ s/>\s+</></sgo;
+
+    return $xml;
+
+$_$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION biblio.flatten_marc ( TEXT ) RETURNS SETOF metabib.full_rec AS $func$
+
+use MARC::Record;
+use MARC::File::XML (BinaryEncoding => 'UTF-8');
+use MARC::Charset;
+
+MARC::Charset->assume_unicode(1);
+
+my $xml = shift;
+my $r = MARC::Record->new_from_xml( $xml );
+
+return_next( { tag => 'LDR', value => $r->leader } );
+
+for my $f ( $r->fields ) {
+       if ($f->is_control_field) {
+               return_next({ tag => $f->tag, value => $f->data });
+       } else {
+               for my $s ($f->subfields) {
+                       return_next({
+                               tag      => $f->tag,
+                               ind1     => $f->indicator(1),
+                               ind2     => $f->indicator(2),
+                               subfield => $s->[0],
+                               value    => $s->[1]
+                       });
+
+                       if ( $f->tag eq '245' and $s->[0] eq 'a' ) {
+                               my $trim = $f->indicator(2) || 0;
+                               return_next({
+                                       tag      => 'tnf',
+                                       ind1     => $f->indicator(1),
+                                       ind2     => $f->indicator(2),
+                                       subfield => 'a',
+                                       value    => substr( $s->[1], $trim )
+                               });
+                       }
+               }
+       }
+}
+
+return undef;
+
+$func$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION authority.flatten_marc ( TEXT ) RETURNS SETOF authority.full_rec AS $func$
+
+use MARC::Record;
+use MARC::File::XML (BinaryEncoding => 'UTF-8');
+use MARC::Charset;
+
+MARC::Charset->assume_unicode(1);
+
+my $xml = shift;
+my $r = MARC::Record->new_from_xml( $xml );
+
+return_next( { tag => 'LDR', value => $r->leader } );
+
+for my $f ( $r->fields ) {
+    if ($f->is_control_field) {
+        return_next({ tag => $f->tag, value => $f->data });
+    } else {
+        for my $s ($f->subfields) {
+            return_next({
+                tag      => $f->tag,
+                ind1     => $f->indicator(1),
+                ind2     => $f->indicator(2),
+                subfield => $s->[0],
+                value    => $s->[1]
+            });
+
+        }
+    }
+}
+
+return undef;
+
+$func$ LANGUAGE PLPERLU;
+
+\qecho Rewriting authority records to include a 901$c, so that
+\qecho they can be used to control bibs.  This may take a while...
+
+UPDATE authority.record_entry SET active = active;
+
 \qecho Upgrade script completed.
 \qecho But wait, there's more: please run reingest-1.6-2.0.pl
 \qecho in order to create an SQL script to run to partially reindex 
index a026d26..fadc392 100644 (file)
@@ -59,6 +59,7 @@ CREATE TABLE config.circ_matrix_matchpoint (
     circ_modifier        TEXT    REFERENCES config.circ_modifier (code) DEFERRABLE INITIALLY DEFERRED,
     marc_type            TEXT,
     marc_form            TEXT,
+    marc_bib_level       TEXT,
     marc_vr_format       TEXT,
     copy_circ_lib        INT     REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
     copy_owning_lib      INT     REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
@@ -82,7 +83,7 @@ CREATE TABLE config.circ_matrix_matchpoint (
 );
 
 -- Nulls don't count for a constraint match, so we have to coalesce them into something that does.
-CREATE UNIQUE INDEX ccmm_once_per_paramset ON config.circ_matrix_matchpoint (org_unit, grp, COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_vr_format, ''), COALESCE(copy_circ_lib::TEXT, ''), COALESCE(copy_owning_lib::TEXT, ''), COALESCE(user_home_ou::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(is_renewal::TEXT, ''), COALESCE(usr_age_lower_bound::TEXT, ''), COALESCE(usr_age_upper_bound::TEXT, '')) WHERE active;
+CREATE UNIQUE INDEX ccmm_once_per_paramset ON config.circ_matrix_matchpoint (org_unit, grp, COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level,''), COALESCE(marc_vr_format, ''), COALESCE(copy_circ_lib::TEXT, ''), COALESCE(copy_owning_lib::TEXT, ''), COALESCE(user_home_ou::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(is_renewal::TEXT, ''), COALESCE(usr_age_lower_bound::TEXT, ''), COALESCE(usr_age_upper_bound::TEXT, '')) WHERE active;
 
 -- Tests for max items out by circ_modifier
 CREATE TABLE config.circ_matrix_circ_mod_test (
@@ -140,6 +141,7 @@ BEGIN
         weights.circ_modifier       := 5.0;
         weights.marc_type           := 4.0;
         weights.marc_form           := 3.0;
+        weights.marc_bib_level      := 2.0;
         weights.marc_vr_format      := 2.0;
         weights.copy_circ_lib       := 8.0;
         weights.copy_owning_lib     := 8.0;
@@ -189,6 +191,7 @@ BEGIN
                 AND (m.circ_modifier            IS NULL OR m.circ_modifier = item_object.circ_modifier)
                 AND (m.marc_type                IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
                 AND (m.marc_form                IS NULL OR m.marc_form = rec_descriptor.item_form)
+                AND (m.marc_bib_level           IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
                 AND (m.marc_vr_format           IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
                 AND (m.ref_flag                 IS NULL OR m.ref_flag = item_object.ref)
           ORDER BY
index 3b954af..b914874 100644 (file)
@@ -43,6 +43,7 @@ CREATE TABLE config.hold_matrix_matchpoint (
     circ_modifier           TEXT    REFERENCES config.circ_modifier (code) DEFERRABLE INITIALLY DEFERRED,
     marc_type               TEXT,
     marc_form               TEXT,
+    marc_bib_level          TEXT,
     marc_vr_format          TEXT,
     juvenile_flag           BOOL,
     ref_flag                BOOL,
@@ -57,7 +58,7 @@ CREATE TABLE config.hold_matrix_matchpoint (
 );
 
 -- Nulls don't count for a constraint match, so we have to coalesce them into something that does.
-CREATE UNIQUE INDEX chmm_once_per_paramset ON config.hold_matrix_matchpoint (COALESCE(user_home_ou::TEXT, ''), COALESCE(request_ou::TEXT, ''), COALESCE(pickup_ou::TEXT, ''), COALESCE(item_owning_ou::TEXT, ''), COALESCE(item_circ_ou::TEXT, ''), COALESCE(usr_grp::TEXT, ''), COALESCE(requestor_grp::TEXT, ''), COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_vr_format, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(ref_flag::TEXT, '')) WHERE active;
+CREATE UNIQUE INDEX chmm_once_per_paramset ON config.hold_matrix_matchpoint (COALESCE(user_home_ou::TEXT, ''), COALESCE(request_ou::TEXT, ''), COALESCE(pickup_ou::TEXT, ''), COALESCE(item_owning_ou::TEXT, ''), COALESCE(item_circ_ou::TEXT, ''), COALESCE(usr_grp::TEXT, ''), COALESCE(requestor_grp::TEXT, ''), COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level, ''), COALESCE(marc_vr_format, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(ref_flag::TEXT, '')) WHERE active;
 
 CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
   RETURNS integer AS
@@ -116,6 +117,7 @@ BEGIN
         weights.circ_modifier   := 4.0;
         weights.marc_type       := 3.0;
         weights.marc_form       := 2.0;
+        weights.marc_bib_level  := 1.0;
         weights.marc_vr_format  := 1.0;
         weights.juvenile_flag   := 4.0;
         weights.ref_flag        := 0.0;
@@ -171,6 +173,7 @@ BEGIN
             AND (m.circ_modifier        IS NULL OR m.circ_modifier = item_object.circ_modifier)
             AND (m.marc_type            IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
             AND (m.marc_form            IS NULL OR m.marc_form = rec_descriptor.item_form)
+            AND (m.marc_bib_level       IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
             AND (m.marc_vr_format       IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
             AND (m.ref_flag             IS NULL OR m.ref_flag = item_object.ref)
       ORDER BY
diff --git a/Open-ILS/src/sql/Pg/2.0-2.1-upgrade-db.sql b/Open-ILS/src/sql/Pg/2.0-2.1-upgrade-db.sql
new file mode 100644 (file)
index 0000000..f134572
--- /dev/null
@@ -0,0 +1,5834 @@
+BEGIN;
+
+-- 0425
+ALTER TABLE permission.grp_tree
+        ADD COLUMN hold_priority INT NOT NULL DEFAULT 0;
+
+-- 0430
+ALTER TABLE config.hold_matrix_matchpoint ADD COLUMN strict_ou_match BOOL NOT NULL DEFAULT FALSE;
+
+-- 0498
+-- Rather than polluting the public schema with general Evergreen
+-- functions, carve out a dedicated schema
+CREATE SCHEMA evergreen;
+
+-- Replace all uses of PostgreSQL's built-in LOWER() function with
+-- a more locale-savvy PLPERLU evergreen.lowercase() function
+CREATE OR REPLACE FUNCTION evergreen.lowercase( TEXT ) RETURNS TEXT AS $$
+    return lc(shift);
+$$ LANGUAGE PLPERLU STRICT IMMUTABLE;
+
+-- 0500
+CREATE OR REPLACE FUNCTION evergreen.change_db_setting(setting_name TEXT, settings TEXT[]) RETURNS VOID AS $$
+BEGIN
+EXECUTE 'ALTER DATABASE ' || quote_ident(current_database()) || ' SET ' || quote_ident(setting_name) || ' = ' || array_to_string(settings, ',');
+END;
+
+$$ LANGUAGE plpgsql;
+
+-- 0501
+SELECT evergreen.change_db_setting('search_path', ARRAY['evergreen','public','pg_catalog']);
+
+-- Fix function breakage due to short search path
+CREATE OR REPLACE FUNCTION evergreen.force_unicode_normal_form(string TEXT, form TEXT) RETURNS TEXT AS $func$
+use Unicode::Normalize 'normalize';
+return normalize($_[1],$_[0]); # reverse the params
+$func$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION evergreen.facet_force_nfc() RETURNS TRIGGER AS $$
+BEGIN
+    NEW.value := force_unicode_normal_form(NEW.value,'NFC');
+    RETURN NEW;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION evergreen.xml_escape(str TEXT) RETURNS text AS $$
+    SELECT REPLACE(REPLACE(REPLACE($1,
+       '&', '&amp;'),
+       '<', '&lt;'),
+       '>', '&gt;');
+$$ LANGUAGE SQL IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION evergreen.maintain_901 () RETURNS TRIGGER AS $func$
+DECLARE
+    use_id_for_tcn BOOLEAN;
+BEGIN
+    -- Remove any existing 901 fields before we insert the authoritative one
+    NEW.marc := REGEXP_REPLACE(NEW.marc, E'<datafield[^>]*?tag="901".+?</datafield>', '', 'g');
+
+    IF TG_TABLE_SCHEMA = 'biblio' THEN
+        -- Set TCN value to record ID?
+        SELECT enabled FROM config.global_flag INTO use_id_for_tcn
+            WHERE name = 'cat.bib.use_id_for_tcn';
+
+        IF use_id_for_tcn = 't' THEN
+            NEW.tcn_value := NEW.id;
+        END IF;
+
+        NEW.marc := REGEXP_REPLACE(
+            NEW.marc,
+            E'(</(?:[^:]*?:)?record>)',
+            E'<datafield tag="901" ind1=" " ind2=" ">' ||
+                '<subfield code="a">' || evergreen.xml_escape(NEW.tcn_value) || E'</subfield>' ||
+                '<subfield code="b">' || evergreen.xml_escape(NEW.tcn_source) || E'</subfield>' ||
+                '<subfield code="c">' || NEW.id || E'</subfield>' ||
+                '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
+                CASE WHEN NEW.owner IS NOT NULL THEN '<subfield code="o">' || NEW.owner || E'</subfield>' ELSE '' END ||
+                CASE WHEN NEW.share_depth IS NOT NULL THEN '<subfield code="d">' || NEW.share_depth || E'</subfield>' ELSE '' END ||
+             E'</datafield>\\1'
+        );
+    ELSIF TG_TABLE_SCHEMA = 'authority' THEN
+        NEW.marc := REGEXP_REPLACE(
+            NEW.marc,
+            E'(</(?:[^:]*?:)?record>)',
+            E'<datafield tag="901" ind1=" " ind2=" ">' ||
+                '<subfield code="c">' || NEW.id || E'</subfield>' ||
+                '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
+             E'</datafield>\\1'
+        );
+    ELSIF TG_TABLE_SCHEMA = 'serial' THEN
+        NEW.marc := REGEXP_REPLACE(
+            NEW.marc,
+            E'(</(?:[^:]*?:)?record>)',
+            E'<datafield tag="901" ind1=" " ind2=" ">' ||
+                '<subfield code="c">' || NEW.id || E'</subfield>' ||
+                '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
+                '<subfield code="o">' || NEW.owning_lib || E'</subfield>' ||
+                CASE WHEN NEW.record IS NOT NULL THEN '<subfield code="r">' || NEW.record || E'</subfield>' ELSE '' END ||
+             E'</datafield>\\1'
+        );
+    ELSE
+        NEW.marc := REGEXP_REPLACE(
+            NEW.marc,
+            E'(</(?:[^:]*?:)?record>)',
+            E'<datafield tag="901" ind1=" " ind2=" ">' ||
+                '<subfield code="c">' || NEW.id || E'</subfield>' ||
+                '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
+             E'</datafield>\\1'
+        );
+    END IF;
+
+    RETURN NEW;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION evergreen.array_remove_item_by_value(inp ANYARRAY, el ANYELEMENT) RETURNS anyarray AS $$ SELECT ARRAY_ACCUM(x.e) FROM UNNEST( $1 ) x(e) WHERE x.e <> $2; $$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION evergreen.lpad_number_substrings( TEXT, TEXT, INT ) RETURNS TEXT AS $$
+    my $string = shift;
+    my $pad = shift;
+    my $len = shift;
+    my $find = $len - 1;
+
+    while ($string =~ /(?:^|\D)(\d{1,$find})(?:$|\D)/) {
+        my $padded = $1;
+        $padded = $pad x ($len - length($padded)) . $padded;
+        $string =~ s/$1/$padded/sg;
+    }
+
+    return $string;
+$$ LANGUAGE PLPERLU;
+
+-- 0477
+ALTER TABLE config.hard_due_date DROP CONSTRAINT hard_due_date_name_check;
+
+-- 0478
+CREATE OR REPLACE FUNCTION public.naco_normalize( TEXT, TEXT ) RETURNS TEXT AS $func$
+
+    use strict;
+    use Unicode::Normalize;
+    use Encode;
+
+    my $str = decode_utf8(shift);
+    my $sf = shift;
+
+    # Apply NACO normalization to input string; based on
+    # http://www.loc.gov/catdir/pcc/naco/SCA_PccNormalization_Final_revised.pdf
+    #
+    # Note that unlike a strict reading of the NACO normalization rules,
+    # output is returned as lowercase instead of uppercase for compatibility
+    # with previous versions of the Evergreen naco_normalize routine.
+
+    # Convert to upper-case first; even though final output will be lowercase, doing this will
+    # ensure that the German eszett (ß) and certain ligatures (ff, fi, ffl, etc.) will be handled correctly.
+    # If there are any bugs in Perl's implementation of upcasing, they will be passed through here.
+    $str = uc $str;
+
+    # remove non-filing strings
+    $str =~ s/\x{0098}.*?\x{009C}//g;
+
+    $str = NFKD($str);
+
+    # additional substitutions - 3.6.
+    $str =~ s/\x{00C6}/AE/g;
+    $str =~ s/\x{00DE}/TH/g;
+    $str =~ s/\x{0152}/OE/g;
+    $str =~ tr/\x{0110}\x{00D0}\x{00D8}\x{0141}\x{2113}\x{02BB}\x{02BC}]['/DDOLl/d;
+
+    # transformations based on Unicode category codes
+    $str =~ s/[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Lm}\p{Mc}\p{Me}\p{Mn}]//g;
+
+       if ($sf && $sf =~ /^a/o) {
+               my $commapos = index($str, ',');
+               if ($commapos > -1) {
+                       if ($commapos != length($str) - 1) {
+                $str =~ s/,/\x07/; # preserve first comma
+                       }
+               }
+       }
+
+    # since we've stripped out the control characters, we can now
+    # use a few as placeholders temporarily
+    $str =~ tr/+&@\x{266D}\x{266F}#/\x01\x02\x03\x04\x05\x06/;
+    $str =~ s/[\p{Pc}\p{Pd}\p{Pe}\p{Pf}\p{Pi}\p{Po}\p{Ps}\p{Sk}\p{Sm}\p{So}\p{Zl}\p{Zp}\p{Zs}]/ /g;
+    $str =~ tr/\x01\x02\x03\x04\x05\x06\x07/+&@\x{266D}\x{266F}#,/;
+
+    # decimal digits
+    $str =~ tr/\x{0660}-\x{0669}\x{06F0}-\x{06F9}\x{07C0}-\x{07C9}\x{0966}-\x{096F}\x{09E6}-\x{09EF}\x{0A66}-\x{0A6F}\x{0AE6}-\x{0AEF}\x{0B66}-\x{0B6F}\x{0BE6}-\x{0BEF}\x{0C66}-\x{0C6F}\x{0CE6}-\x{0CEF}\x{0D66}-\x{0D6F}\x{0E50}-\x{0E59}\x{0ED0}-\x{0ED9}\x{0F20}-\x{0F29}\x{1040}-\x{1049}\x{1090}-\x{1099}\x{17E0}-\x{17E9}\x{1810}-\x{1819}\x{1946}-\x{194F}\x{19D0}-\x{19D9}\x{1A80}-\x{1A89}\x{1A90}-\x{1A99}\x{1B50}-\x{1B59}\x{1BB0}-\x{1BB9}\x{1C40}-\x{1C49}\x{1C50}-\x{1C59}\x{A620}-\x{A629}\x{A8D0}-\x{A8D9}\x{A900}-\x{A909}\x{A9D0}-\x{A9D9}\x{AA50}-\x{AA59}\x{ABF0}-\x{ABF9}\x{FF10}-\x{FF19}/0-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-9/;
+
+    # intentionally skipping step 8 of the NACO algorithm; if the string
+    # gets normalized away, that's fine.
+
+    # leading and trailing spaces
+    $str =~ s/\s+/ /g;
+    $str =~ s/^\s+//;
+    $str =~ s/\s+$//g;
+
+    return lc $str;
+$func$ LANGUAGE 'plperlu' STRICT IMMUTABLE;
+
+-- 0479
+CREATE OR REPLACE FUNCTION permission.grp_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
+    WITH RECURSIVE grp_ancestors_distance(id, distance) AS (
+            SELECT $1, 0
+        UNION
+            SELECT pgt.parent, gad.distance+1
+            FROM permission.grp_tree pgt JOIN grp_ancestors_distance gad ON pgt.id = gad.id
+            WHERE pgt.parent IS NOT NULL
+    )
+    SELECT * FROM grp_ancestors_distance;
+$$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION permission.grp_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
+    WITH RECURSIVE grp_descendants_distance(id, distance) AS (
+            SELECT $1, 0
+        UNION
+            SELECT pgt.id, gdd.distance+1
+            FROM permission.grp_tree pgt JOIN grp_descendants_distance gdd ON pgt.parent = gdd.id
+    )
+    SELECT * FROM grp_descendants_distance;
+$$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION actor.org_unit_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
+    WITH RECURSIVE org_unit_ancestors_distance(id, distance) AS (
+            SELECT $1, 0
+        UNION
+            SELECT ou.parent_ou, ouad.distance+1
+            FROM actor.org_unit ou JOIN org_unit_ancestors_distance ouad ON ou.id = ouad.id
+            WHERE ou.parent_ou IS NOT NULL
+    )
+    SELECT * FROM org_unit_ancestors_distance;
+$$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION actor.org_unit_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
+    WITH RECURSIVE org_unit_descendants_distance(id, distance) AS (
+            SELECT $1, 0
+        UNION
+            SELECT ou.id, oudd.distance+1
+            FROM actor.org_unit ou JOIN org_unit_descendants_distance oudd ON ou.parent_ou = oudd.id
+    )
+    SELECT * FROM org_unit_descendants_distance;
+$$ LANGUAGE SQL STABLE;
+
+ALTER TABLE config.circ_matrix_matchpoint
+    ADD COLUMN user_home_ou         INT     REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED;
+
+CREATE TABLE config.circ_matrix_weights (
+    id                      SERIAL  PRIMARY KEY,
+    name                    TEXT    NOT NULL UNIQUE,
+    org_unit                NUMERIC(6,2)   NOT NULL,
+    grp                     NUMERIC(6,2)   NOT NULL,
+    circ_modifier           NUMERIC(6,2)   NOT NULL,
+    marc_type               NUMERIC(6,2)   NOT NULL,
+    marc_form               NUMERIC(6,2)   NOT NULL,
+    marc_vr_format          NUMERIC(6,2)   NOT NULL,
+    copy_circ_lib           NUMERIC(6,2)   NOT NULL,
+    copy_owning_lib         NUMERIC(6,2)   NOT NULL,
+    user_home_ou            NUMERIC(6,2)   NOT NULL,
+    ref_flag                NUMERIC(6,2)   NOT NULL,
+    juvenile_flag           NUMERIC(6,2)   NOT NULL,
+    is_renewal              NUMERIC(6,2)   NOT NULL,
+    usr_age_lower_bound     NUMERIC(6,2)   NOT NULL,
+    usr_age_upper_bound     NUMERIC(6,2)   NOT NULL
+);
+
+CREATE TABLE config.hold_matrix_weights (
+    id                      SERIAL  PRIMARY KEY,
+    name                    TEXT    NOT NULL UNIQUE,
+    user_home_ou            NUMERIC(6,2)   NOT NULL,
+    request_ou              NUMERIC(6,2)   NOT NULL,
+    pickup_ou               NUMERIC(6,2)   NOT NULL,
+    item_owning_ou          NUMERIC(6,2)   NOT NULL,
+    item_circ_ou            NUMERIC(6,2)   NOT NULL,
+    usr_grp                 NUMERIC(6,2)   NOT NULL,
+    requestor_grp           NUMERIC(6,2)   NOT NULL,
+    circ_modifier           NUMERIC(6,2)   NOT NULL,
+    marc_type               NUMERIC(6,2)   NOT NULL,
+    marc_form               NUMERIC(6,2)   NOT NULL,
+    marc_vr_format          NUMERIC(6,2)   NOT NULL,
+    juvenile_flag           NUMERIC(6,2)   NOT NULL,
+    ref_flag                NUMERIC(6,2)   NOT NULL
+);
+
+CREATE TABLE config.weight_assoc (
+    id                      SERIAL  PRIMARY KEY,
+    active                  BOOL    NOT NULL,
+    org_unit                INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    circ_weights            INT     REFERENCES config.circ_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
+    hold_weights            INT     REFERENCES config.hold_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
+);
+CREATE UNIQUE INDEX cwa_one_active_per_ou ON config.weight_assoc (org_unit) WHERE active;
+
+INSERT INTO config.circ_matrix_weights(name, org_unit, grp, circ_modifier, marc_type, marc_form, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_upper_bound, usr_age_lower_bound) VALUES 
+    ('Default', 10.0, 11.0, 5.0, 4.0, 3.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
+    ('Org_Unit_First', 11.0, 10.0, 5.0, 4.0, 3.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
+    ('Item_Owner_First', 8.0, 8.0, 5.0, 4.0, 3.0, 2.0, 10.0, 11.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
+    ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
+
+INSERT INTO config.hold_matrix_weights(name, user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_vr_format, juvenile_flag, ref_flag) VALUES
+    ('Default', 5.0, 5.0, 5.0, 5.0, 5.0, 7.0, 8.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
+    ('Item_Owner_First', 5.0, 5.0, 5.0, 8.0, 7.0, 5.0, 5.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
+    ('User_Before_Requestor', 5.0, 5.0, 5.0, 5.0, 5.0, 8.0, 7.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
+    ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
+
+INSERT INTO config.weight_assoc(active, org_unit, circ_weights, hold_weights) VALUES
+    (true, 1, 1, 1);
+
+-- 0480
+CREATE OR REPLACE FUNCTION actor.usr_purge_data(
+       src_usr  IN INTEGER,
+       specified_dest_usr IN INTEGER
+) RETURNS VOID AS $$
+DECLARE
+       suffix TEXT;
+       renamable_row RECORD;
+       dest_usr INTEGER;
+BEGIN
+
+       IF specified_dest_usr IS NULL THEN
+               dest_usr := 1; -- Admin user on stock installs
+       ELSE
+               dest_usr := specified_dest_usr;
+       END IF;
+
+       UPDATE actor.usr SET
+               active = FALSE,
+               card = NULL,
+               mailing_address = NULL,
+               billing_address = NULL
+       WHERE id = src_usr;
+
+       -- acq.*
+       UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
+       UPDATE acq.lineitem SET creator = dest_usr WHERE creator = src_usr;
+       UPDATE acq.lineitem SET editor = dest_usr WHERE editor = src_usr;
+       UPDATE acq.lineitem SET selector = dest_usr WHERE selector = src_usr;
+       UPDATE acq.lineitem_note SET creator = dest_usr WHERE creator = src_usr;
+       UPDATE acq.lineitem_note SET editor = dest_usr WHERE editor = src_usr;
+       DELETE FROM acq.lineitem_usr_attr_definition WHERE usr = src_usr;
+
+       -- Update with a rename to avoid collisions
+       FOR renamable_row in
+               SELECT id, name
+               FROM   acq.picklist
+               WHERE  owner = src_usr
+       LOOP
+               suffix := ' (' || src_usr || ')';
+               LOOP
+                       BEGIN
+                               UPDATE  acq.picklist
+                               SET     owner = dest_usr, name = name || suffix
+                               WHERE   id = renamable_row.id;
+                       EXCEPTION WHEN unique_violation THEN
+                               suffix := suffix || ' ';
+                               CONTINUE;
+                       END;
+                       EXIT;
+               END LOOP;
+       END LOOP;
+
+       UPDATE acq.picklist SET creator = dest_usr WHERE creator = src_usr;
+       UPDATE acq.picklist SET editor = dest_usr WHERE editor = src_usr;
+       UPDATE acq.po_note SET creator = dest_usr WHERE creator = src_usr;
+       UPDATE acq.po_note SET editor = dest_usr WHERE editor = src_usr;
+       UPDATE acq.purchase_order SET owner = dest_usr WHERE owner = src_usr;
+       UPDATE acq.purchase_order SET creator = dest_usr WHERE creator = src_usr;
+       UPDATE acq.purchase_order SET editor = dest_usr WHERE editor = src_usr;
+       UPDATE acq.claim_event SET creator = dest_usr WHERE creator = src_usr;
+
+       -- action.*
+       DELETE FROM action.circulation WHERE usr = src_usr;
+       UPDATE action.circulation SET circ_staff = dest_usr WHERE circ_staff = src_usr;
+       UPDATE action.circulation SET checkin_staff = dest_usr WHERE checkin_staff = src_usr;
+       UPDATE action.hold_notification SET notify_staff = dest_usr WHERE notify_staff = src_usr;
+       UPDATE action.hold_request SET fulfillment_staff = dest_usr WHERE fulfillment_staff = src_usr;
+       UPDATE action.hold_request SET requestor = dest_usr WHERE requestor = src_usr;
+       DELETE FROM action.hold_request WHERE usr = src_usr;
+       UPDATE action.in_house_use SET staff = dest_usr WHERE staff = src_usr;
+       UPDATE action.non_cat_in_house_use SET staff = dest_usr WHERE staff = src_usr;
+       DELETE FROM action.non_cataloged_circulation WHERE patron = src_usr;
+       UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
+       DELETE FROM action.survey_response WHERE usr = src_usr;
+       UPDATE action.fieldset SET owner = dest_usr WHERE owner = src_usr;
+
+       -- actor.*
+       DELETE FROM actor.card WHERE usr = src_usr;
+       DELETE FROM actor.stat_cat_entry_usr_map WHERE target_usr = src_usr;
+
+       -- The following update is intended to avoid transient violations of a foreign
+       -- key constraint, whereby actor.usr_address references itself.  It may not be
+       -- necessary, but it does no harm.
+       UPDATE actor.usr_address SET replaces = NULL
+               WHERE usr = src_usr AND replaces IS NOT NULL;
+       DELETE FROM actor.usr_address WHERE usr = src_usr;
+       DELETE FROM actor.usr_note WHERE usr = src_usr;
+       UPDATE actor.usr_note SET creator = dest_usr WHERE creator = src_usr;
+       DELETE FROM actor.usr_org_unit_opt_in WHERE usr = src_usr;
+       UPDATE actor.usr_org_unit_opt_in SET staff = dest_usr WHERE staff = src_usr;
+       DELETE FROM actor.usr_setting WHERE usr = src_usr;
+       DELETE FROM actor.usr_standing_penalty WHERE usr = src_usr;
+       UPDATE actor.usr_standing_penalty SET staff = dest_usr WHERE staff = src_usr;
+
+       -- asset.*
+       UPDATE asset.call_number SET creator = dest_usr WHERE creator = src_usr;
+       UPDATE asset.call_number SET editor = dest_usr WHERE editor = src_usr;
+       UPDATE asset.call_number_note SET creator = dest_usr WHERE creator = src_usr;
+       UPDATE asset.copy SET creator = dest_usr WHERE creator = src_usr;
+       UPDATE asset.copy SET editor = dest_usr WHERE editor = src_usr;
+       UPDATE asset.copy_note SET creator = dest_usr WHERE creator = src_usr;
+
+       -- auditor.*
+       DELETE FROM auditor.actor_usr_address_history WHERE id = src_usr;
+       DELETE FROM auditor.actor_usr_history WHERE id = src_usr;
+       UPDATE auditor.asset_call_number_history SET creator = dest_usr WHERE creator = src_usr;
+       UPDATE auditor.asset_call_number_history SET editor  = dest_usr WHERE editor  = src_usr;
+       UPDATE auditor.asset_copy_history SET creator = dest_usr WHERE creator = src_usr;
+       UPDATE auditor.asset_copy_history SET editor  = dest_usr WHERE editor  = src_usr;
+       UPDATE auditor.biblio_record_entry_history SET creator = dest_usr WHERE creator = src_usr;
+       UPDATE auditor.biblio_record_entry_history SET editor  = dest_usr WHERE editor  = src_usr;
+
+       -- biblio.*
+       UPDATE biblio.record_entry SET creator = dest_usr WHERE creator = src_usr;
+       UPDATE biblio.record_entry SET editor = dest_usr WHERE editor = src_usr;
+       UPDATE biblio.record_note SET creator = dest_usr WHERE creator = src_usr;
+       UPDATE biblio.record_note SET editor = dest_usr WHERE editor = src_usr;
+
+       -- container.*
+       -- Update buckets with a rename to avoid collisions
+       FOR renamable_row in
+               SELECT id, name
+               FROM   container.biblio_record_entry_bucket
+               WHERE  owner = src_usr
+       LOOP
+               suffix := ' (' || src_usr || ')';
+               LOOP
+                       BEGIN
+                               UPDATE  container.biblio_record_entry_bucket
+                               SET     owner = dest_usr, name = name || suffix
+                               WHERE   id = renamable_row.id;
+                       EXCEPTION WHEN unique_violation THEN
+                               suffix := suffix || ' ';
+                               CONTINUE;
+                       END;
+                       EXIT;
+               END LOOP;
+       END LOOP;
+
+       FOR renamable_row in
+               SELECT id, name
+               FROM   container.call_number_bucket
+               WHERE  owner = src_usr
+       LOOP
+               suffix := ' (' || src_usr || ')';
+               LOOP
+                       BEGIN
+                               UPDATE  container.call_number_bucket
+                               SET     owner = dest_usr, name = name || suffix
+                               WHERE   id = renamable_row.id;
+                       EXCEPTION WHEN unique_violation THEN
+                               suffix := suffix || ' ';
+                               CONTINUE;
+                       END;
+                       EXIT;
+               END LOOP;
+       END LOOP;
+
+       FOR renamable_row in
+               SELECT id, name
+               FROM   container.copy_bucket
+               WHERE  owner = src_usr
+       LOOP
+               suffix := ' (' || src_usr || ')';
+               LOOP
+                       BEGIN
+                               UPDATE  container.copy_bucket
+                               SET     owner = dest_usr, name = name || suffix
+                               WHERE   id = renamable_row.id;
+                       EXCEPTION WHEN unique_violation THEN
+                               suffix := suffix || ' ';
+                               CONTINUE;
+                       END;
+                       EXIT;
+               END LOOP;
+       END LOOP;
+
+       FOR renamable_row in
+               SELECT id, name
+               FROM   container.user_bucket
+               WHERE  owner = src_usr
+       LOOP
+               suffix := ' (' || src_usr || ')';
+               LOOP
+                       BEGIN
+                               UPDATE  container.user_bucket
+                               SET     owner = dest_usr, name = name || suffix
+                               WHERE   id = renamable_row.id;
+                       EXCEPTION WHEN unique_violation THEN
+                               suffix := suffix || ' ';
+                               CONTINUE;
+                       END;
+                       EXIT;
+               END LOOP;
+       END LOOP;
+
+       DELETE FROM container.user_bucket_item WHERE target_user = src_usr;
+
+       -- money.*
+       DELETE FROM money.billable_xact WHERE usr = src_usr;
+       DELETE FROM money.collections_tracker WHERE usr = src_usr;
+       UPDATE money.collections_tracker SET collector = dest_usr WHERE collector = src_usr;
+
+       -- permission.*
+       DELETE FROM permission.usr_grp_map WHERE usr = src_usr;
+       DELETE FROM permission.usr_object_perm_map WHERE usr = src_usr;
+       DELETE FROM permission.usr_perm_map WHERE usr = src_usr;
+       DELETE FROM permission.usr_work_ou_map WHERE usr = src_usr;
+
+       -- reporter.*
+       -- Update with a rename to avoid collisions
+       BEGIN
+               FOR renamable_row in
+                       SELECT id, name
+                       FROM   reporter.output_folder
+                       WHERE  owner = src_usr
+               LOOP
+                       suffix := ' (' || src_usr || ')';
+                       LOOP
+                               BEGIN
+                                       UPDATE  reporter.output_folder
+                                       SET     owner = dest_usr, name = name || suffix
+                                       WHERE   id = renamable_row.id;
+                               EXCEPTION WHEN unique_violation THEN
+                                       suffix := suffix || ' ';
+                                       CONTINUE;
+                               END;
+                               EXIT;
+                       END LOOP;
+               END LOOP;
+       EXCEPTION WHEN undefined_table THEN
+               -- do nothing
+       END;
+
+       BEGIN
+               UPDATE reporter.report SET owner = dest_usr WHERE owner = src_usr;
+       EXCEPTION WHEN undefined_table THEN
+               -- do nothing
+       END;
+
+       -- Update with a rename to avoid collisions
+       BEGIN
+               FOR renamable_row in
+                       SELECT id, name
+                       FROM   reporter.report_folder
+                       WHERE  owner = src_usr
+               LOOP
+                       suffix := ' (' || src_usr || ')';
+                       LOOP
+                               BEGIN
+                                       UPDATE  reporter.report_folder
+                                       SET     owner = dest_usr, name = name || suffix
+                                       WHERE   id = renamable_row.id;
+                               EXCEPTION WHEN unique_violation THEN
+                                       suffix := suffix || ' ';
+                                       CONTINUE;
+                               END;
+                               EXIT;
+                       END LOOP;
+               END LOOP;
+       EXCEPTION WHEN undefined_table THEN
+               -- do nothing
+       END;
+
+       BEGIN
+               UPDATE reporter.schedule SET runner = dest_usr WHERE runner = src_usr;
+       EXCEPTION WHEN undefined_table THEN
+               -- do nothing
+       END;
+
+       BEGIN
+               UPDATE reporter.template SET owner = dest_usr WHERE owner = src_usr;
+       EXCEPTION WHEN undefined_table THEN
+               -- do nothing
+       END;
+
+       -- Update with a rename to avoid collisions
+       BEGIN
+               FOR renamable_row in
+                       SELECT id, name
+                       FROM   reporter.template_folder
+                       WHERE  owner = src_usr
+               LOOP
+                       suffix := ' (' || src_usr || ')';
+                       LOOP
+                               BEGIN
+                                       UPDATE  reporter.template_folder
+                                       SET     owner = dest_usr, name = name || suffix
+                                       WHERE   id = renamable_row.id;
+                               EXCEPTION WHEN unique_violation THEN
+                                       suffix := suffix || ' ';
+                                       CONTINUE;
+                               END;
+                               EXIT;
+                       END LOOP;
+               END LOOP;
+       EXCEPTION WHEN undefined_table THEN
+       -- do nothing
+       END;
+
+       -- vandelay.*
+       -- Update with a rename to avoid collisions
+       FOR renamable_row in
+               SELECT id, name
+               FROM   vandelay.queue
+               WHERE  owner = src_usr
+       LOOP
+               suffix := ' (' || src_usr || ')';
+               LOOP
+                       BEGIN
+                               UPDATE  vandelay.queue
+                               SET     owner = dest_usr, name = name || suffix
+                               WHERE   id = renamable_row.id;
+                       EXCEPTION WHEN unique_violation THEN
+                               suffix := suffix || ' ';
+                               CONTINUE;
+                       END;
+                       EXIT;
+               END LOOP;
+       END LOOP;
+
+END;
+$$ LANGUAGE plpgsql;
+
+-- 0482
+-- Drop old (non-functional) constraints
+
+ALTER TABLE config.circ_matrix_matchpoint
+    DROP CONSTRAINT ep_once_per_grp_loc_mod_marc;
+
+ALTER TABLE config.hold_matrix_matchpoint
+    DROP CONSTRAINT hous_once_per_grp_loc_mod_marc;
+
+-- Clean up tables before making normalized index
+
+CREATE OR REPLACE FUNCTION action.cleanup_matrix_matchpoints() RETURNS void AS $func$
+DECLARE
+    temp_row    RECORD;
+BEGIN
+    -- Circ Matrix
+    FOR temp_row IN
+        SELECT org_unit, grp, circ_modifier, marc_type, marc_form, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_lower_bound, usr_age_upper_bound, COUNT(id) as rowcount, MIN(id) as firstrow
+        FROM config.circ_matrix_matchpoint
+        WHERE active
+        GROUP BY org_unit, grp, circ_modifier, marc_type, marc_form, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_lower_bound, usr_age_upper_bound
+        HAVING COUNT(id) > 1 LOOP
+
+        UPDATE config.circ_matrix_matchpoint SET active=false
+            WHERE id > temp_row.firstrow
+                AND org_unit = temp_row.org_unit
+                AND grp = temp_row.grp
+                AND circ_modifier       IS NOT DISTINCT FROM temp_row.circ_modifier
+                AND marc_type           IS NOT DISTINCT FROM temp_row.marc_type
+                AND marc_form           IS NOT DISTINCT FROM temp_row.marc_form
+                AND marc_vr_format      IS NOT DISTINCT FROM temp_row.marc_vr_format
+                AND copy_circ_lib       IS NOT DISTINCT FROM temp_row.copy_circ_lib
+                AND copy_owning_lib     IS NOT DISTINCT FROM temp_row.copy_owning_lib
+                AND user_home_ou        IS NOT DISTINCT FROM temp_row.user_home_ou
+                AND ref_flag            IS NOT DISTINCT FROM temp_row.ref_flag
+                AND juvenile_flag       IS NOT DISTINCT FROM temp_row.juvenile_flag
+                AND is_renewal          IS NOT DISTINCT FROM temp_row.is_renewal
+                AND usr_age_lower_bound IS NOT DISTINCT FROM temp_row.usr_age_lower_bound
+                AND usr_age_upper_bound IS NOT DISTINCT FROM temp_row.usr_age_upper_bound;
+    END LOOP;
+
+    -- Hold Matrix
+    FOR temp_row IN
+        SELECT user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_vr_format, juvenile_flag, ref_flag, COUNT(id) as rowcount, MIN(id) as firstrow
+        FROM config.hold_matrix_matchpoint
+        WHERE active
+        GROUP BY user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_vr_format, juvenile_flag, ref_flag
+        HAVING COUNT(id) > 1 LOOP
+
+        UPDATE config.hold_matrix_matchpoint SET active=false
+            WHERE id > temp_row.firstrow
+                AND user_home_ou        IS NOT DISTINCT FROM temp_row.user_home_ou
+                AND request_ou          IS NOT DISTINCT FROM temp_row.request_ou
+                AND pickup_ou           IS NOT DISTINCT FROM temp_row.pickup_ou
+                AND item_owning_ou      IS NOT DISTINCT FROM temp_row.item_owning_ou
+                AND item_circ_ou        IS NOT DISTINCT FROM temp_row.item_circ_ou
+                AND usr_grp             IS NOT DISTINCT FROM temp_row.usr_grp
+                AND requestor_grp       IS NOT DISTINCT FROM temp_row.requestor_grp
+                AND circ_modifier       IS NOT DISTINCT FROM temp_row.circ_modifier
+                AND marc_type           IS NOT DISTINCT FROM temp_row.marc_type
+                AND marc_form           IS NOT DISTINCT FROM temp_row.marc_form
+                AND marc_vr_format      IS NOT DISTINCT FROM temp_row.marc_vr_format
+                AND juvenile_flag       IS NOT DISTINCT FROM temp_row.juvenile_flag
+                AND ref_flag            IS NOT DISTINCT FROM temp_row.ref_flag;
+    END LOOP;
+END;
+$func$ LANGUAGE plpgsql;
+
+SELECT action.cleanup_matrix_matchpoints();
+
+DROP FUNCTION IF EXISTS action.cleanup_matrix_matchpoints();
+
+-- Create Normalized indexes
+
+CREATE UNIQUE INDEX ccmm_once_per_paramset ON config.circ_matrix_matchpoint (org_unit, grp, COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_vr_format, ''), COALESCE(copy_circ_lib::TEXT, ''), COALESCE(copy_owning_lib::TEXT, ''), COALESCE(user_home_ou::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(is_renewal::TEXT, ''), COALESCE(usr_age_lower_bound::TEXT, ''), COALESCE(usr_age_upper_bound::TEXT, '')) WHERE active;
+
+CREATE UNIQUE INDEX chmm_once_per_paramset ON config.hold_matrix_matchpoint (COALESCE(user_home_ou::TEXT, ''), COALESCE(request_ou::TEXT, ''), COALESCE(pickup_ou::TEXT, ''), COALESCE(item_owning_ou::TEXT, ''), COALESCE(item_circ_ou::TEXT, ''), COALESCE(usr_grp::TEXT, ''), COALESCE(requestor_grp::TEXT, ''), COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_vr_format, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(ref_flag::TEXT, '')) WHERE active;
+
+-- 0484
+DROP FUNCTION asset.metarecord_copy_count ( INT, BIGINT, BOOL );
+DROP FUNCTION asset.record_copy_count ( INT, BIGINT, BOOL );
+
+DROP FUNCTION asset.opac_ou_record_copy_count (INT, BIGINT);
+CREATE OR REPLACE FUNCTION asset.opac_ou_record_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+    ans RECORD;
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+
+    FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
+        RETURN QUERY
+        SELECT  ans.depth,
+                ans.id,
+                COUNT( av.id ),
+                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                COUNT( av.id ),
+                trans
+          FROM  
+                actor.org_unit_descendants(ans.id) d
+                JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
+                JOIN asset.copy cp ON (cp.id = av.copy_id)
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+DROP FUNCTION asset.opac_lasso_record_copy_count (INT, BIGINT);
+CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+    ans RECORD;
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+
+    FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
+        RETURN QUERY
+        SELECT  -1,
+                ans.id,
+                COUNT( av.id ),
+                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                COUNT( av.id ),
+                trans
+          FROM  
+                actor.org_unit_descendants(ans.id) d
+                JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
+                JOIN asset.copy cp ON (cp.id = av.copy_id)
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+DROP FUNCTION asset.staff_ou_record_copy_count (INT, BIGINT);
+
+DROP FUNCTION asset.staff_lasso_record_copy_count (INT, BIGINT);
+
+CREATE OR REPLACE FUNCTION asset.record_copy_count ( place INT, rid BIGINT, staff BOOL) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+BEGIN
+    IF staff IS TRUE THEN
+        IF place > 0 THEN
+            RETURN QUERY SELECT * FROM asset.staff_ou_record_copy_count( place, rid );
+        ELSE
+            RETURN QUERY SELECT * FROM asset.staff_lasso_record_copy_count( -place, rid );
+        END IF;
+    ELSE
+        IF place > 0 THEN
+            RETURN QUERY SELECT * FROM asset.opac_ou_record_copy_count( place, rid );
+        ELSE
+            RETURN QUERY SELECT * FROM asset.opac_lasso_record_copy_count( -place, rid );
+        END IF;
+    END IF;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+DROP FUNCTION asset.opac_ou_metarecord_copy_count (INT, BIGINT);
+CREATE OR REPLACE FUNCTION asset.opac_ou_metarecord_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+    ans RECORD;
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+
+    FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
+        RETURN QUERY
+        SELECT  ans.depth,
+                ans.id,
+                COUNT( av.id ),
+                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                COUNT( av.id ),
+                trans
+          FROM
+                actor.org_unit_descendants(ans.id) d
+                JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
+                JOIN asset.copy cp ON (cp.id = av.copy_id)
+                JOIN metabib.metarecord_source_map m ON (m.source = av.record)
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+DROP FUNCTION asset.opac_lasso_metarecord_copy_count (INT, BIGINT);
+CREATE OR REPLACE FUNCTION asset.opac_lasso_metarecord_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+    ans RECORD;
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+
+    FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
+        RETURN QUERY
+        SELECT  -1,
+                ans.id,
+                COUNT( av.id ),
+                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                COUNT( av.id ),
+                trans
+          FROM
+                actor.org_unit_descendants(ans.id) d
+                JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
+                JOIN asset.copy cp ON (cp.id = av.copy_id)
+                JOIN metabib.metarecord_source_map m ON (m.source = av.record)
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+DROP FUNCTION asset.staff_lasso_metarecord_copy_count (INT, BIGINT);
+
+CREATE OR REPLACE FUNCTION asset.metarecord_copy_count ( place INT, rid BIGINT, staff BOOL) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+BEGIN
+    IF staff IS TRUE THEN
+        IF place > 0 THEN
+            RETURN QUERY SELECT * FROM asset.staff_ou_metarecord_copy_count( place, rid );
+        ELSE
+            RETURN QUERY SELECT * FROM asset.staff_lasso_metarecord_copy_count( -place, rid );
+        END IF;
+    ELSE
+        IF place > 0 THEN
+            RETURN QUERY SELECT * FROM asset.opac_ou_metarecord_copy_count( place, rid );
+        ELSE
+            RETURN QUERY SELECT * FROM asset.opac_lasso_metarecord_copy_count( -place, rid );
+        END IF;
+    END IF;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+-- 0485
+CREATE OR REPLACE VIEW reporter.simple_record AS
+SELECT r.id,
+       s.metarecord,
+       r.fingerprint,
+       r.quality,
+       r.tcn_source,
+       r.tcn_value,
+       title.value AS title,
+       uniform_title.value AS uniform_title,
+       author.value AS author,
+       publisher.value AS publisher,
+       SUBSTRING(pubdate.value FROM $$\d+$$) AS pubdate,
+       series_title.value AS series_title,
+       series_statement.value AS series_statement,
+       summary.value AS summary,
+       ARRAY_ACCUM( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) AS isbn,
+       ARRAY_ACCUM( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) AS issn,
+       ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '650' AND subfield = 'a' AND record = r.id)) AS topic_subject,
+       ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '651' AND subfield = 'a' AND record = r.id)) AS geographic_subject,
+       ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '655' AND subfield = 'a' AND record = r.id)) AS genre,
+       ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '600' AND subfield = 'a' AND record = r.id)) AS name_subject,
+       ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '610' AND subfield = 'a' AND record = r.id)) AS corporate_subject,
+       ARRAY((SELECT value FROM metabib.full_rec WHERE tag = '856' AND subfield IN ('3','y','u') AND record = r.id ORDER BY CASE WHEN subfield IN ('3','y') THEN 0 ELSE 1 END)) AS external_uri
+  FROM biblio.record_entry r
+       JOIN metabib.metarecord_source_map s ON (s.source = r.id)
+       LEFT JOIN metabib.full_rec uniform_title ON (r.id = uniform_title.record AND uniform_title.tag = '240' AND uniform_title.subfield = 'a')
+       LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
+       LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag = '100' AND author.subfield = 'a')
+       LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND publisher.tag = '260' AND publisher.subfield = 'b')
+       LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND pubdate.tag = '260' AND pubdate.subfield = 'c')
+       LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
+       LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
+       LEFT JOIN metabib.full_rec series_title ON (r.id = series_title.record AND series_title.tag IN ('830','440') AND series_title.subfield = 'a')
+       LEFT JOIN metabib.full_rec series_statement ON (r.id = series_statement.record AND series_statement.tag = '490' AND series_statement.subfield = 'a')
+       LEFT JOIN metabib.full_rec summary ON (r.id = summary.record AND summary.tag = '520' AND summary.subfield = 'a')
+  GROUP BY 1,2,3,4,5,6,7,8,9,10,11,12,13,14;
+
+CREATE OR REPLACE VIEW reporter.old_super_simple_record AS
+SELECT  r.id,
+    r.fingerprint,
+    r.quality,
+    r.tcn_source,
+    r.tcn_value,
+    FIRST(title.value) AS title,
+    FIRST(author.value) AS author,
+    ARRAY_TO_STRING(ARRAY_ACCUM( DISTINCT publisher.value), ', ') AS publisher,
+    ARRAY_TO_STRING(ARRAY_ACCUM( DISTINCT SUBSTRING(pubdate.value FROM $$\d+$$) ), ', ') AS pubdate,
+    ARRAY_ACCUM( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) AS isbn,
+    ARRAY_ACCUM( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) AS issn
+  FROM  biblio.record_entry r
+    LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
+    LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag IN ('100','110','111') AND author.subfield = 'a')
+    LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND publisher.tag = '260' AND publisher.subfield = 'b')
+    LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND pubdate.tag = '260' AND pubdate.subfield = 'c')
+    LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
+    LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
+  GROUP BY 1,2,3,4,5;
+
+-- 0486
+ALTER TABLE money.credit_card_payment ADD COLUMN cc_order_number TEXT;
+
+-- 0487
+-- Circ matchpoint table changes
+ALTER TABLE config.circ_matrix_matchpoint
+    ALTER COLUMN circulate DROP NOT NULL, -- Fallthrough enable
+    ALTER COLUMN circulate DROP DEFAULT, -- Stop defaulting to true to enable default to fallthrough
+    ALTER COLUMN duration_rule DROP NOT NULL, -- Fallthrough enable
+    ALTER COLUMN recurring_fine_rule DROP NOT NULL, -- Fallthrough enable
+    ALTER COLUMN max_fine_rule DROP NOT NULL, -- Fallthrough enable
+    ADD COLUMN renewals INT; -- Renewals override
+
+-- Changing return types requires explicit dropping of old versions
+DROP FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, match_item BIGINT, match_user INT, renewal BOOL );
+DROP FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL );
+DROP FUNCTION action.item_user_circ_test( INT, BIGINT, INT );
+DROP FUNCTION action.item_user_renew_test( INT, BIGINT, INT );
+
+-- New return types
+CREATE TYPE action.found_circ_matrix_matchpoint AS ( success BOOL, matchpoint config.circ_matrix_matchpoint, buildrows INT[] );
+
+-- Helper function - For manual calling, it can be easier to pass in IDs instead of objects
+CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.found_circ_matrix_matchpoint AS $func$
+DECLARE
+    item_object asset.copy%ROWTYPE;
+    user_object actor.usr%ROWTYPE;
+BEGIN
+    SELECT INTO item_object * FROM asset.copy  WHERE id = match_item;
+    SELECT INTO user_object * FROM actor.usr   WHERE id = match_user;
+
+    RETURN QUERY SELECT * FROM action.find_circ_matrix_matchpoint( context_ou, item_object, user_object, renewal );
+END;
+$func$ LANGUAGE plpgsql;
+
+CREATE TYPE action.circ_matrix_test_result AS ( success BOOL, fail_part TEXT, buildrows INT[], matchpoint INT, circulate BOOL, duration_rule INT, recurring_fine_rule INT, max_fine_rule INT, hard_due_date INT, renewals INT, grace_period INTERVAL );
+
+CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$
+DECLARE
+    user_object             actor.usr%ROWTYPE;
+    standing_penalty        config.standing_penalty%ROWTYPE;
+    item_object             asset.copy%ROWTYPE;
+    item_status_object      config.copy_status%ROWTYPE;
+    item_location_object    asset.copy_location%ROWTYPE;
+    result                  action.circ_matrix_test_result;
+    circ_test               action.found_circ_matrix_matchpoint;
+    circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
+    out_by_circ_mod         config.circ_matrix_circ_mod_test%ROWTYPE;
+    circ_mod_map            config.circ_matrix_circ_mod_test_map%ROWTYPE;
+    hold_ratio              action.hold_stats%ROWTYPE;
+    penalty_type            TEXT;
+    items_out               INT;
+    context_org_list        INT[];
+    done                    BOOL := FALSE;
+BEGIN
+    -- Assume success unless we hit a failure condition
+    result.success := TRUE;
+
+    -- Fail if the user is BARRED
+    SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
+
+    -- Fail if we couldn't find the user 
+    IF user_object.id IS NULL THEN
+        result.fail_part := 'no_user';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
+
+    -- Fail if we couldn't find the item 
+    IF item_object.id IS NULL THEN
+        result.fail_part := 'no_item';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    IF user_object.barred IS TRUE THEN
+        result.fail_part := 'actor.usr.barred';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the item can't circulate
+    IF item_object.circulate IS FALSE THEN
+        result.fail_part := 'asset.copy.circulate';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the item isn't in a circulateable status on a non-renewal
+    IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
+        result.fail_part := 'asset.copy.status';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    ELSIF renewal AND item_object.status <> 1 THEN
+        result.fail_part := 'asset.copy.status';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the item can't circulate because of the shelving location
+    SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
+    IF item_location_object.circulate IS FALSE THEN
+        result.fail_part := 'asset.copy_location.circulate';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
+
+    circ_matchpoint             := circ_test.matchpoint;
+    result.matchpoint           := circ_matchpoint.id;
+    result.circulate            := circ_matchpoint.circulate;
+    result.duration_rule        := circ_matchpoint.duration_rule;
+    result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
+    result.max_fine_rule        := circ_matchpoint.max_fine_rule;
+    result.hard_due_date        := circ_matchpoint.hard_due_date;
+    result.renewals             := circ_matchpoint.renewals;
+    result.buildrows            := circ_test.buildrows;
+
+    -- Fail if we couldn't find a matchpoint
+    IF circ_test.success = false THEN
+        result.fail_part := 'no_matchpoint';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN; -- All tests after this point require a matchpoint. No sense in running on an incomplete or missing one.
+    END IF;
+
+    -- Apparently....use the circ matchpoint org unit to determine what org units are valid.
+    SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( circ_matchpoint.org_unit );
+
+    IF renewal THEN
+        penalty_type = '%RENEW%';
+    ELSE
+        penalty_type = '%CIRC%';
+    END IF;
+
+    FOR standing_penalty IN
+        SELECT  DISTINCT csp.*
+          FROM  actor.usr_standing_penalty usp
+                JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
+          WHERE usr = match_user
+                AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
+                AND (usp.stop_date IS NULL or usp.stop_date > NOW())
+                AND csp.block_list LIKE penalty_type LOOP
+
+        result.fail_part := standing_penalty.name;
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END LOOP;
+
+    -- Fail if the test is set to hard non-circulating
+    IF circ_matchpoint.circulate IS FALSE THEN
+        result.fail_part := 'config.circ_matrix_test.circulate';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the total copy-hold ratio is too low
+    IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
+        SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
+        IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
+            result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END IF;
+
+    -- Fail if the available copy-hold ratio is too low
+    IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
+        IF hold_ratio.hold_count IS NULL THEN
+            SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
+        END IF;
+        IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
+            result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END IF;
+
+    -- Fail if the user has too many items with specific circ_modifiers checked out
+    FOR out_by_circ_mod IN SELECT * FROM config.circ_matrix_circ_mod_test WHERE matchpoint = circ_matchpoint.id LOOP
+        SELECT  INTO items_out COUNT(*)
+          FROM  action.circulation circ
+            JOIN asset.copy cp ON (cp.id = circ.target_copy)
+          WHERE circ.usr = match_user
+               AND circ.circ_lib IN ( SELECT * FROM unnest(context_org_list) )
+            AND circ.checkin_time IS NULL
+            AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
+            AND cp.circ_modifier IN (SELECT circ_mod FROM config.circ_matrix_circ_mod_test_map WHERE circ_mod_test = out_by_circ_mod.id);
+        IF items_out >= out_by_circ_mod.items_out THEN
+            result.fail_part := 'config.circ_matrix_circ_mod_test';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END LOOP;
+
+    -- If we passed everything, return the successful matchpoint id
+    IF NOT done THEN
+        RETURN NEXT result;
+    END IF;
+
+    RETURN;
+END;
+$func$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION action.item_user_circ_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
+    SELECT * FROM action.item_user_circ_test( $1, $2, $3, FALSE );
+$func$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION action.item_user_renew_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
+    SELECT * FROM action.item_user_circ_test( $1, $2, $3, TRUE );
+$func$ LANGUAGE SQL;
+
+-- 0490
+CREATE OR REPLACE FUNCTION asset.staff_ou_record_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE         
+    ans RECORD; 
+    trans INT;
+BEGIN           
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+
+    FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
+        RETURN QUERY
+        SELECT  ans.depth,
+                ans.id,
+                COUNT( cp.id ),
+                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                COUNT( cp.id ),
+                trans
+          FROM
+                actor.org_unit_descendants(ans.id) d
+                JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
+                JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION asset.staff_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+    ans RECORD;
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+
+    FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
+        RETURN QUERY
+        SELECT  -1,
+                ans.id,
+                COUNT( cp.id ),
+                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                COUNT( cp.id ),
+                trans
+          FROM
+                actor.org_unit_descendants(ans.id) d
+                JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
+                JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+DROP FUNCTION asset.staff_ou_metarecord_copy_count (INT, BIGINT);
+CREATE OR REPLACE FUNCTION asset.staff_ou_metarecord_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE         
+    ans RECORD; 
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+
+    FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
+        RETURN QUERY
+        SELECT  ans.depth,
+                ans.id,
+                COUNT( cp.id ),
+                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                COUNT( cp.id ),
+                trans
+          FROM
+                actor.org_unit_descendants(ans.id) d
+                JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
+                JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
+                JOIN metabib.metarecord_source_map m ON (m.source = cn.record)
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION asset.staff_lasso_metarecord_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+    ans RECORD;
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+
+    FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
+        RETURN QUERY
+        SELECT  -1,
+                ans.id,
+                COUNT( cp.id ),
+                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                COUNT( cp.id ),
+                trans
+          FROM
+                actor.org_unit_descendants(ans.id) d
+                JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
+                JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
+                JOIN metabib.metarecord_source_map m ON (m.source = cn.record)
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+
+-- 0493
+UPDATE config.org_unit_setting_type
+    SET description = 'Amount of time before a hold expires at which point the patron should be alerted. Examples: "5 days", "1 hour"'
+    WHERE label = 'Holds: Expire Alert Interval';
+
+UPDATE config.org_unit_setting_type
+    SET description = 'When predicting the amount of time a patron will be waiting for a hold to be fulfilled, this is the default estimated length of time to assume an item will be checked out. Examples: "3 weeks", "7 days"'
+    WHERE label = 'Holds: Default Estimated Wait';
+
+UPDATE config.org_unit_setting_type
+    SET description = 'When predicting the amount of time a patron will be waiting for a hold to be fulfilled, this is the minimum estimated length of time to assume an item will be checked out. Examples: "1 week", "5 days"'
+    WHERE label = 'Holds: Minimum Estimated Wait';
+
+UPDATE config.org_unit_setting_type
+    SET description = 'The purpose is to provide an interval of time after an item goes into the on-holds-shelf status before it appears to patrons that it is actually on the holds shelf.  This gives staff time to process the item before it shows as ready-for-pickup. Examples: "5 days", "1 hour"'
+    WHERE label = 'Hold Shelf Status Delay';
+
+-- 0494
+UPDATE config.metabib_field
+    SET xpath = $$//mods32:mods/mods32:subject$$
+    WHERE field_class = 'subject' AND name = 'complete';
+
+UPDATE config.metabib_field
+    SET xpath = $$//marc:datafield[@tag='099']$$
+    WHERE field_class = 'identifier' AND name = 'bibcn';
+
+-- 0495
+CREATE TABLE config.record_attr_definition (
+    name        TEXT    PRIMARY KEY,
+    label       TEXT    NOT NULL, -- I18N
+    description TEXT,
+    filter      BOOL    NOT NULL DEFAULT TRUE,  -- becomes QP filter if true
+    sorter      BOOL    NOT NULL DEFAULT FALSE, -- becomes QP sort() axis if true
+
+-- For pre-extracted fields. Takes the first occurance, uses naive subfield ordering
+    tag         TEXT, -- LIKE format
+    sf_list     TEXT, -- pile-o-values, like 'abcd' for a and b and c and d
+
+-- This is used for both tag/sf and xpath entries
+    joiner      TEXT,
+
+-- For xpath-extracted attrs
+    xpath       TEXT,
+    format      TEXT    REFERENCES config.xml_transform (name) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    start_pos   INT,
+    string_len  INT,
+
+-- For fixed fields
+    fixed_field TEXT, -- should exist in config.marc21_ff_pos_map.fixed_field
+
+-- For phys-char fields
+    phys_char_sf    INT REFERENCES config.marc21_physical_characteristic_subfield_map (id)
+);
+
+CREATE TABLE config.record_attr_index_norm_map (
+    id      SERIAL  PRIMARY KEY,
+    attr    TEXT    NOT NULL REFERENCES config.record_attr_definition (name) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    norm    INT     NOT NULL REFERENCES config.index_normalizer (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    params  TEXT,
+    pos     INT     NOT NULL DEFAULT 0
+);
+
+CREATE TABLE config.coded_value_map (
+    id          SERIAL  PRIMARY KEY,
+    ctype       TEXT    NOT NULL REFERENCES config.record_attr_definition (name) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    code        TEXT    NOT NULL,
+    value       TEXT    NOT NULL,
+    description TEXT
+);
+
+-- record attributes
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('alph','Alph','Alph');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('audience','Audn','Audn');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('bib_level','BLvl','BLvl');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('biog','Biog','Biog');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('conf','Conf','Conf');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('control_type','Ctrl','Ctrl');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('ctry','Ctry','Ctry');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('date1','Date1','Date1');
+INSERT INTO config.record_attr_definition (name,label,fixed_field,sorter,filter) values ('pubdate','Pub Date','Date1',TRUE,FALSE);
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('date2','Date2','Date2');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('cat_form','Desc','Desc');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('pub_status','DtSt','DtSt');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('enc_level','ELvl','ELvl');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('fest','Fest','Fest');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_form','Form','Form');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('gpub','GPub','GPub');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('ills','Ills','Ills');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('indx','Indx','Indx');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_lang','Lang','Lang');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('lit_form','LitF','LitF');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('mrec','MRec','MRec');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('ff_sl','S/L','S/L');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('type_mat','TMat','TMat');
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_type','Type','Type');
+INSERT INTO config.record_attr_definition (name,label,phys_char_sf) values ('vr_format','Videorecording format',72);
+INSERT INTO config.record_attr_definition (name,label,sorter,filter,tag) values ('titlesort','Title',TRUE,FALSE,'tnf');
+INSERT INTO config.record_attr_definition (name,label,sorter,filter,tag) values ('authorsort','Author',TRUE,FALSE,'1%');
+
+INSERT INTO config.coded_value_map (ctype,code,value,description)
+    SELECT 'item_lang' AS ctype, code, value, NULL FROM config.language_map
+        UNION
+    SELECT 'bib_level' AS ctype, code, value, NULL FROM config.bib_level_map
+        UNION
+    SELECT 'item_form' AS ctype, code, value, NULL FROM config.item_form_map
+        UNION
+    SELECT 'item_type' AS ctype, code, value, NULL FROM config.item_type_map
+        UNION
+    SELECT 'lit_form' AS ctype, code, value, description FROM config.lit_form_map
+        UNION
+    SELECT 'audience' AS ctype, code, value, description FROM config.audience_map
+        UNION
+    SELECT 'vr_format' AS ctype, code, value, NULL FROM config.videorecording_format_map;
+
+ALTER TABLE config.i18n_locale DROP CONSTRAINT i18n_locale_marc_code_fkey;
+
+ALTER TABLE config.circ_matrix_matchpoint DROP CONSTRAINT circ_matrix_matchpoint_marc_form_fkey;
+ALTER TABLE config.circ_matrix_matchpoint DROP CONSTRAINT circ_matrix_matchpoint_marc_type_fkey;
+ALTER TABLE config.circ_matrix_matchpoint DROP CONSTRAINT circ_matrix_matchpoint_marc_vr_format_fkey;
+
+ALTER TABLE config.hold_matrix_matchpoint DROP CONSTRAINT hold_matrix_matchpoint_marc_form_fkey;
+ALTER TABLE config.hold_matrix_matchpoint DROP CONSTRAINT hold_matrix_matchpoint_marc_type_fkey;
+ALTER TABLE config.hold_matrix_matchpoint DROP CONSTRAINT hold_matrix_matchpoint_marc_vr_format_fkey;
+
+DROP TABLE config.language_map;
+DROP TABLE config.bib_level_map;
+DROP TABLE config.item_form_map;
+DROP TABLE config.item_type_map;
+DROP TABLE config.lit_form_map;
+DROP TABLE config.audience_map;
+DROP TABLE config.videorecording_format_map;
+
+UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'clm.value' AND ccvm.ctype = 'item_lang' AND identity_value = ccvm.code;
+UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cblvl.value' AND ccvm.ctype = 'bib_level' AND identity_value = ccvm.code;
+UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cifm.value' AND ccvm.ctype = 'item_form' AND identity_value = ccvm.code;
+UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'citm.value' AND ccvm.ctype = 'item_type' AND identity_value = ccvm.code;
+UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'clfm.value' AND ccvm.ctype = 'lit_form' AND identity_value = ccvm.code;
+UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cam.value' AND ccvm.ctype = 'audience' AND identity_value = ccvm.code;
+UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cvrfm.value' AND ccvm.ctype = 'vr_format' AND identity_value = ccvm.code;
+
+UPDATE config.i18n_core SET fq_field = 'ccvm.description', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'clfm.description' AND ccvm.ctype = 'lit_form' AND identity_value = ccvm.code;
+UPDATE config.i18n_core SET fq_field = 'ccvm.description', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cam.description' AND ccvm.ctype = 'audience' AND identity_value = ccvm.code;
+
+CREATE VIEW config.language_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'item_lang';
+CREATE VIEW config.bib_level_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'bib_level';
+CREATE VIEW config.item_form_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'item_form';
+CREATE VIEW config.item_type_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'item_type';
+CREATE VIEW config.lit_form_map AS SELECT code, value, description FROM config.coded_value_map WHERE ctype = 'lit_form';
+CREATE VIEW config.audience_map AS SELECT code, value, description FROM config.coded_value_map WHERE ctype = 'audience';
+CREATE VIEW config.videorecording_format_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'vr_format';
+
+CREATE TABLE metabib.record_attr (
+       id              BIGINT  PRIMARY KEY REFERENCES biblio.record_entry (id) ON DELETE CASCADE,
+       attrs   HSTORE  NOT NULL DEFAULT ''::HSTORE
+);
+CREATE INDEX metabib_svf_attrs_idx ON metabib.record_attr USING GIST (attrs);
+CREATE INDEX metabib_svf_date1_idx ON metabib.record_attr ( (attrs->'date1') );
+CREATE INDEX metabib_svf_dates_idx ON metabib.record_attr ( (attrs->'date1'), (attrs->'date2') );
+
+INSERT INTO metabib.record_attr (id,attrs)
+    SELECT mrd.record, hstore(mrd) - '{id,record}'::TEXT[] FROM metabib.rec_descriptor mrd;
+
+-- Back-compat view ... we're moving to an HSTORE world
+CREATE TYPE metabib.rec_desc_type AS (
+    item_type       TEXT,
+    item_form       TEXT,
+    bib_level       TEXT,
+    control_type    TEXT,
+    char_encoding   TEXT,
+    enc_level       TEXT,
+    audience        TEXT,
+    lit_form        TEXT,
+    type_mat        TEXT,
+    cat_form        TEXT,
+    pub_status      TEXT,
+    item_lang       TEXT,
+    vr_format       TEXT,
+    date1           TEXT,
+    date2           TEXT
+);
+
+DROP TABLE metabib.rec_descriptor CASCADE;
+
+CREATE VIEW metabib.rec_descriptor AS
+    SELECT  id,
+            id AS record,
+            (populate_record(NULL::metabib.rec_desc_type, attrs)).*
+      FROM  metabib.record_attr;
+
+CREATE OR REPLACE FUNCTION vandelay.marc21_record_type( marc TEXT ) RETURNS config.marc21_rec_type_map AS $func$
+DECLARE
+    ldr         TEXT;
+    tval        TEXT;
+    tval_rec    RECORD;
+    bval        TEXT;
+    bval_rec    RECORD;
+    retval      config.marc21_rec_type_map%ROWTYPE;
+BEGIN
+    ldr := oils_xpath_string( '//*[local-name()="leader"]', marc );
+
+    IF ldr IS NULL OR ldr = '' THEN
+        SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
+        RETURN retval;
+    END IF;
+
+    SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
+    SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
+
+
+    tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
+    bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
+
+    -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
+
+    SELECT * INTO retval FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
+
+
+    IF retval.code IS NULL THEN
+        SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
+    END IF;
+
+    RETURN retval;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION biblio.marc21_record_type( rid BIGINT ) RETURNS config.marc21_rec_type_map AS $func$
+    SELECT * FROM vandelay.marc21_record_type( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
+$func$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
+DECLARE
+    rtype       TEXT;
+    ff_pos      RECORD;
+    tag_data    RECORD;
+    val         TEXT;
+BEGIN
+    rtype := (vandelay.marc21_record_type( marc )).code;
+    FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
+        FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
+            val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
+            RETURN val;
+        END LOOP;
+        val := REPEAT( ff_pos.default_val, ff_pos.length );
+        RETURN val;
+    END LOOP;
+
+    RETURN NULL;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field( rid BIGINT, ff TEXT ) RETURNS TEXT AS $func$
+    SELECT * FROM vandelay.marc21_extract_fixed_field( (SELECT marc FROM biblio.record_entry WHERE id = $1), $2 );
+$func$ LANGUAGE SQL;
+
+CREATE TYPE biblio.record_ff_map AS (record BIGINT, ff_name TEXT, ff_value TEXT);
+CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT ) RETURNS SETOF biblio.record_ff_map AS $func$
+DECLARE
+    tag_data    TEXT;
+    rtype       TEXT;
+    ff_pos      RECORD;
+    output      biblio.record_ff_map%ROWTYPE;
+BEGIN
+    rtype := (vandelay.marc21_record_type( marc )).code;
+
+    FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE rec_type = rtype ORDER BY tag DESC LOOP
+        output.ff_name  := ff_pos.fixed_field;
+        output.ff_value := NULL;
+
+        FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(tag) || '"]/text()', marc ) ) x(value) LOOP
+            output.ff_value := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
+            IF output.ff_value IS NULL THEN output.ff_value := REPEAT( ff_pos.default_val, ff_pos.length ); END IF;
+            RETURN NEXT output;
+            output.ff_value := NULL;
+        END LOOP;
+
+    END LOOP;
+
+    RETURN;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION biblio.marc21_extract_all_fixed_fields( rid BIGINT ) RETURNS SETOF biblio.record_ff_map AS $func$
+    SELECT $1 AS record, ff_name, ff_value FROM vandelay.marc21_extract_all_fixed_fields( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
+$func$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION vandelay.marc21_physical_characteristics( marc TEXT) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
+DECLARE
+    rowid   INT := 0;
+    _007    TEXT;
+    ptype   config.marc21_physical_characteristic_type_map%ROWTYPE;
+    psf     config.marc21_physical_characteristic_subfield_map%ROWTYPE;
+    pval    config.marc21_physical_characteristic_value_map%ROWTYPE;
+    retval  biblio.marc21_physical_characteristics%ROWTYPE;
+BEGIN
+
+    _007 := oils_xpath_string( '//*[@tag="007"]', marc );
+
+    IF _007 IS NOT NULL AND _007 <> '' THEN
+        SELECT * INTO ptype FROM config.marc21_physical_characteristic_type_map WHERE ptype_key = SUBSTRING( _007, 1, 1 );
+
+        IF ptype.ptype_key IS NOT NULL THEN
+            FOR psf IN SELECT * FROM config.marc21_physical_characteristic_subfield_map WHERE ptype_key = ptype.ptype_key LOOP
+                SELECT * INTO pval FROM config.marc21_physical_characteristic_value_map WHERE ptype_subfield = psf.id AND value = SUBSTRING( _007, psf.start_pos + 1, psf.length );
+
+                IF pval.id IS NOT NULL THEN
+                    rowid := rowid + 1;
+                    retval.id := rowid;
+                    retval.ptype := ptype.ptype_key;
+                    retval.subfield := psf.id;
+                    retval.value := pval.id;
+                    RETURN NEXT retval;
+                END IF;
+
+            END LOOP;
+        END IF;
+    END IF;
+
+    RETURN;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION biblio.marc21_physical_characteristics( rid BIGINT ) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
+    SELECT id, $1 AS record, ptype, subfield, value FROM vandelay.marc21_physical_characteristics( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
+$func$ LANGUAGE SQL;
+
+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
+        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;
+
+DROP FUNCTION metabib.reingest_metabib_rec_descriptor( bib_id BIGINT );
+
+CREATE OR REPLACE FUNCTION public.approximate_date( TEXT, TEXT ) RETURNS TEXT AS $func$
+        SELECT REGEXP_REPLACE( $1, E'\\D', $2, 'g' );
+$func$ LANGUAGE SQL STRICT IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION public.approximate_low_date( TEXT ) RETURNS TEXT AS $func$
+        SELECT approximate_date( $1, '0');
+$func$ LANGUAGE SQL STRICT IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION public.approximate_high_date( TEXT ) RETURNS TEXT AS $func$
+        SELECT approximate_date( $1, '9');
+$func$ LANGUAGE SQL STRICT IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION public.integer_or_null( TEXT ) RETURNS TEXT AS $func$
+        SELECT CASE WHEN $1 ~ E'^\\d+$' THEN $1 ELSE NULL END
+$func$ LANGUAGE SQL STRICT IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION public.content_or_null( TEXT ) RETURNS TEXT AS $func$
+        SELECT CASE WHEN $1 ~ E'^\\s*$' THEN NULL ELSE $1 END
+$func$ LANGUAGE SQL STRICT IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION public.force_to_isbn13( TEXT ) RETURNS TEXT AS $func$
+    use Business::ISBN;
+    use strict;
+    use warnings;
+
+    # Find the first ISBN, force it to ISBN13 and return it
+
+    my $input = shift;
+
+    foreach my $word (split(/\s/, $input)) {
+        my $isbn = Business::ISBN->new($word);
+
+        # First check the checksum; if it is not valid, fix it and add the original
+        # bad-checksum ISBN to the output
+        if ($isbn && $isbn->is_valid_checksum() == Business::ISBN::BAD_CHECKSUM) {
+            $isbn->fix_checksum();
+        }
+
+        # If we now have a valid ISBN, force it to ISBN13 and return it
+        return $isbn->as_isbn13->isbn if ($isbn && $isbn->is_valid());
+    }
+    return undef;
+$func$ LANGUAGE PLPERLU;
+
+COMMENT ON FUNCTION public.force_to_isbn13(TEXT) IS $$
+/*
+ * Copyright (C) 2011 Equinox Software
+ * Mike Rylander <mrylander@gmail.com>
+ *
+ * Inspired by translate_isbn1013
+ *
+ * The force_to_isbn13 function takes an input ISBN and returns the ISBN13
+ * version without hypens and with a repaired checksum if the checksum was bad
+ */
+$$;
+
+-- 0496
+UPDATE config.metabib_field
+    SET xpath = $$//marc:datafield[@tag='024' and @ind1='1']/marc:subfield[@code='a' or @code='z']$$
+    WHERE field_class = 'identifier' AND name = 'upc';
+
+UPDATE config.metabib_field
+    SET xpath = $$//marc:datafield[@tag='024' and @ind1='2']/marc:subfield[@code='a' or @code='z']$$
+    WHERE field_class = 'identifier' AND name = 'ismn';
+
+UPDATE config.metabib_field
+    SET xpath = $$//marc:datafield[@tag='024' and @ind1='3']/marc:subfield[@code='a' or @code='z']$$
+    WHERE field_class = 'identifier' AND name = 'ean';
+
+UPDATE config.metabib_field
+    SET xpath = $$//marc:datafield[@tag='024' and @ind1='0']/marc:subfield[@code='a' or @code='z']$$
+    WHERE field_class = 'identifier' AND name = 'isrc';
+
+UPDATE config.metabib_field
+    SET xpath = $$//marc:datafield[@tag='024' and @ind1='4']/marc:subfield[@code='a' or @code='z']$$
+    WHERE field_class = 'identifier' AND name = 'sici';
+
+-- 0497
+INSERT into config.org_unit_setting_type
+( name, label, description, datatype ) VALUES
+
+( 'ui.patron.edit.au.active.show',
+    oils_i18n_gettext('ui.patron.edit.au.active.show', 'GUI: Show active field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.active.show', 'The active field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.active.suggest',
+    oils_i18n_gettext('ui.patron.edit.au.active.suggest', 'GUI: Suggest active field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.active.suggest', 'The active field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.alert_message.show',
+    oils_i18n_gettext('ui.patron.edit.au.alert_message.show', 'GUI: Show alert_message field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.alert_message.show', 'The alert_message field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.alert_message.suggest',
+    oils_i18n_gettext('ui.patron.edit.au.alert_message.suggest', 'GUI: Suggest alert_message field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.alert_message.suggest', 'The alert_message field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.alias.show',
+    oils_i18n_gettext('ui.patron.edit.au.alias.show', 'GUI: Show alias field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.alias.show', 'The alias field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.alias.suggest',
+    oils_i18n_gettext('ui.patron.edit.au.alias.suggest', 'GUI: Suggest alias field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.alias.suggest', 'The alias field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.barred.show',
+    oils_i18n_gettext('ui.patron.edit.au.barred.show', 'GUI: Show barred field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.barred.show', 'The barred field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.barred.suggest',
+    oils_i18n_gettext('ui.patron.edit.au.barred.suggest', 'GUI: Suggest barred field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.barred.suggest', 'The barred field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.claims_never_checked_out_count.show',
+    oils_i18n_gettext('ui.patron.edit.au.claims_never_checked_out_count.show', 'GUI: Show claims_never_checked_out_count field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.claims_never_checked_out_count.show', 'The claims_never_checked_out_count field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.claims_never_checked_out_count.suggest',
+    oils_i18n_gettext('ui.patron.edit.au.claims_never_checked_out_count.suggest', 'GUI: Suggest claims_never_checked_out_count field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.claims_never_checked_out_count.suggest', 'The claims_never_checked_out_count field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.claims_returned_count.show',
+    oils_i18n_gettext('ui.patron.edit.au.claims_returned_count.show', 'GUI: Show claims_returned_count field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.claims_returned_count.show', 'The claims_returned_count field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.claims_returned_count.suggest',
+    oils_i18n_gettext('ui.patron.edit.au.claims_returned_count.suggest', 'GUI: Suggest claims_returned_count field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.claims_returned_count.suggest', 'The claims_returned_count field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.day_phone.example',
+    oils_i18n_gettext('ui.patron.edit.au.day_phone.example', 'GUI: Example for day_phone field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.day_phone.example', 'The Example for validation on the day_phone field in patron registration.', 'coust', 'description'),
+    'string'),
+( 'ui.patron.edit.au.day_phone.regex',
+    oils_i18n_gettext('ui.patron.edit.au.day_phone.regex', 'GUI: Regex for day_phone field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.day_phone.regex', 'The Regular Expression for validation on the day_phone field in patron registration.', 'coust', 'description'),
+    'string'),
+( 'ui.patron.edit.au.day_phone.require',
+    oils_i18n_gettext('ui.patron.edit.au.day_phone.require', 'GUI: Require day_phone field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.day_phone.require', 'The day_phone field will be required on the patron registration screen.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.day_phone.show',
+    oils_i18n_gettext('ui.patron.edit.au.day_phone.show', 'GUI: Show day_phone field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.day_phone.show', 'The day_phone field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.day_phone.suggest',
+    oils_i18n_gettext('ui.patron.edit.au.day_phone.suggest', 'GUI: Suggest day_phone field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.day_phone.suggest', 'The day_phone field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.dob.calendar',
+    oils_i18n_gettext('ui.patron.edit.au.dob.calendar', 'GUI: Show calendar widget for dob field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.dob.calendar', 'If set the calendar widget will appear when editing the dob field on the patron registration form.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.dob.require',
+    oils_i18n_gettext('ui.patron.edit.au.dob.require', 'GUI: Require dob field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.dob.require', 'The dob field will be required on the patron registration screen.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.dob.show',
+    oils_i18n_gettext('ui.patron.edit.au.dob.show', 'GUI: Show dob field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.dob.show', 'The dob field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.dob.suggest',
+    oils_i18n_gettext('ui.patron.edit.au.dob.suggest', 'GUI: Suggest dob field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.dob.suggest', 'The dob field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.email.example',
+    oils_i18n_gettext('ui.patron.edit.au.email.example', 'GUI: Example for email field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.email.example', 'The Example for validation on the email field in patron registration.', 'coust', 'description'),
+    'string'),
+( 'ui.patron.edit.au.email.regex',
+    oils_i18n_gettext('ui.patron.edit.au.email.regex', 'GUI: Regex for email field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.email.regex', 'The Regular Expression for validation on the email field in patron registration.', 'coust', 'description'),
+    'string'),
+( 'ui.patron.edit.au.email.require',
+    oils_i18n_gettext('ui.patron.edit.au.email.require', 'GUI: Require email field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.email.require', 'The email field will be required on the patron registration screen.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.email.show',
+    oils_i18n_gettext('ui.patron.edit.au.email.show', 'GUI: Show email field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.email.show', 'The email field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.email.suggest',
+    oils_i18n_gettext('ui.patron.edit.au.email.suggest', 'GUI: Suggest email field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.email.suggest', 'The email field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.evening_phone.example',
+    oils_i18n_gettext('ui.patron.edit.au.evening_phone.example', 'GUI: Example for evening_phone field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.evening_phone.example', 'The Example for validation on the evening_phone field in patron registration.', 'coust', 'description'),
+    'string'),
+( 'ui.patron.edit.au.evening_phone.regex',
+    oils_i18n_gettext('ui.patron.edit.au.evening_phone.regex', 'GUI: Regex for evening_phone field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.evening_phone.regex', 'The Regular Expression for validation on the evening_phone field in patron registration.', 'coust', 'description'),
+    'string'),
+( 'ui.patron.edit.au.evening_phone.require',
+    oils_i18n_gettext('ui.patron.edit.au.evening_phone.require', 'GUI: Require evening_phone field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.evening_phone.require', 'The evening_phone field will be required on the patron registration screen.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.evening_phone.show',
+    oils_i18n_gettext('ui.patron.edit.au.evening_phone.show', 'GUI: Show evening_phone field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.evening_phone.show', 'The evening_phone field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.evening_phone.suggest',
+    oils_i18n_gettext('ui.patron.edit.au.evening_phone.suggest', 'GUI: Suggest evening_phone field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.evening_phone.suggest', 'The evening_phone field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.ident_value.show',
+    oils_i18n_gettext('ui.patron.edit.au.ident_value.show', 'GUI: Show ident_value field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.ident_value.show', 'The ident_value field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.ident_value.suggest',
+    oils_i18n_gettext('ui.patron.edit.au.ident_value.suggest', 'GUI: Suggest ident_value field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.ident_value.suggest', 'The ident_value field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.ident_value2.show',
+    oils_i18n_gettext('ui.patron.edit.au.ident_value2.show', 'GUI: Show ident_value2 field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.ident_value2.show', 'The ident_value2 field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.ident_value2.suggest',
+    oils_i18n_gettext('ui.patron.edit.au.ident_value2.suggest', 'GUI: Suggest ident_value2 field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.ident_value2.suggest', 'The ident_value2 field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.juvenile.show',
+    oils_i18n_gettext('ui.patron.edit.au.juvenile.show', 'GUI: Show juvenile field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.juvenile.show', 'The juvenile field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.juvenile.suggest',
+    oils_i18n_gettext('ui.patron.edit.au.juvenile.suggest', 'GUI: Suggest juvenile field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.juvenile.suggest', 'The juvenile field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.master_account.show',
+    oils_i18n_gettext('ui.patron.edit.au.master_account.show', 'GUI: Show master_account field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.master_account.show', 'The master_account field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.master_account.suggest',
+    oils_i18n_gettext('ui.patron.edit.au.master_account.suggest', 'GUI: Suggest master_account field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.master_account.suggest', 'The master_account field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.other_phone.example',
+    oils_i18n_gettext('ui.patron.edit.au.other_phone.example', 'GUI: Example for other_phone field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.other_phone.example', 'The Example for validation on the other_phone field in patron registration.', 'coust', 'description'),
+    'string'),
+( 'ui.patron.edit.au.other_phone.regex',
+    oils_i18n_gettext('ui.patron.edit.au.other_phone.regex', 'GUI: Regex for other_phone field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.other_phone.regex', 'The Regular Expression for validation on the other_phone field in patron registration.', 'coust', 'description'),
+    'string'),
+( 'ui.patron.edit.au.other_phone.require',
+    oils_i18n_gettext('ui.patron.edit.au.other_phone.require', 'GUI: Require other_phone field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.other_phone.require', 'The other_phone field will be required on the patron registration screen.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.other_phone.show',
+    oils_i18n_gettext('ui.patron.edit.au.other_phone.show', 'GUI: Show other_phone field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.other_phone.show', 'The other_phone field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.other_phone.suggest',
+    oils_i18n_gettext('ui.patron.edit.au.other_phone.suggest', 'GUI: Suggest other_phone field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.other_phone.suggest', 'The other_phone field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.second_given_name.show',
+    oils_i18n_gettext('ui.patron.edit.au.second_given_name.show', 'GUI: Show second_given_name field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.second_given_name.show', 'The second_given_name field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.second_given_name.suggest',
+    oils_i18n_gettext('ui.patron.edit.au.second_given_name.suggest', 'GUI: Suggest second_given_name field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.second_given_name.suggest', 'The second_given_name field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.suffix.show',
+    oils_i18n_gettext('ui.patron.edit.au.suffix.show', 'GUI: Show suffix field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.suffix.show', 'The suffix field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.au.suffix.suggest',
+    oils_i18n_gettext('ui.patron.edit.au.suffix.suggest', 'GUI: Suggest suffix field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.au.suffix.suggest', 'The suffix field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.aua.county.require',
+    oils_i18n_gettext('ui.patron.edit.aua.county.require', 'GUI: Require county field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.aua.county.require', 'The county field will be required on the patron registration screen.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.aua.post_code.example',
+    oils_i18n_gettext('ui.patron.edit.aua.post_code.example', 'GUI: Example for post_code field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.aua.post_code.example', 'The Example for validation on the post_code field in patron registration.', 'coust', 'description'),
+    'string'),
+( 'ui.patron.edit.aua.post_code.regex',
+    oils_i18n_gettext('ui.patron.edit.aua.post_code.regex', 'GUI: Regex for post_code field on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.aua.post_code.regex', 'The Regular Expression for validation on the post_code field in patron registration.', 'coust', 'description'),
+    'string'),
+( 'ui.patron.edit.default_suggested',
+    oils_i18n_gettext('ui.patron.edit.default_suggested', 'GUI: Default showing suggested patron registration fields', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.default_suggested', 'Instead of All fields, show just suggested fields in patron registration by default.', 'coust', 'description'),
+    'bool'),
+( 'ui.patron.edit.phone.example',
+    oils_i18n_gettext('ui.patron.edit.phone.example', 'GUI: Example for phone fields on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.phone.example', 'The Example for validation on phone fields in patron registration. Applies to all phone fields without their own setting.', 'coust', 'description'),
+    'string'),
+( 'ui.patron.edit.phone.regex',
+    oils_i18n_gettext('ui.patron.edit.phone.regex', 'GUI: Regex for phone fields on patron registration', 'coust', 'label'),
+    oils_i18n_gettext('ui.patron.edit.phone.regex', 'The Regular Expression for validation on phone fields in patron registration. Applies to all phone fields without their own setting.', 'coust', 'description'),
+    'string');
+
+-- update actor.usr_address indexes
+DROP INDEX IF EXISTS actor.actor_usr_addr_street1_idx;
+DROP INDEX IF EXISTS actor.actor_usr_addr_street2_idx;
+DROP INDEX IF EXISTS actor.actor_usr_addr_city_idx;
+DROP INDEX IF EXISTS actor.actor_usr_addr_state_idx; 
+DROP INDEX IF EXISTS actor.actor_usr_addr_post_code_idx;
+
+CREATE INDEX actor_usr_addr_street1_idx ON actor.usr_address (evergreen.lowercase(street1));
+CREATE INDEX actor_usr_addr_street2_idx ON actor.usr_address (evergreen.lowercase(street2));
+CREATE INDEX actor_usr_addr_city_idx ON actor.usr_address (evergreen.lowercase(city));
+CREATE INDEX actor_usr_addr_state_idx ON actor.usr_address (evergreen.lowercase(state));
+CREATE INDEX actor_usr_addr_post_code_idx ON actor.usr_address (evergreen.lowercase(post_code));
+
+-- update actor.usr indexes
+DROP INDEX IF EXISTS actor.actor_usr_first_given_name_idx;
+DROP INDEX IF EXISTS actor.actor_usr_second_given_name_idx;
+DROP INDEX IF EXISTS actor.actor_usr_family_name_idx;
+DROP INDEX IF EXISTS actor.actor_usr_email_idx;
+DROP INDEX IF EXISTS actor.actor_usr_day_phone_idx;
+DROP INDEX IF EXISTS actor.actor_usr_evening_phone_idx;
+DROP INDEX IF EXISTS actor.actor_usr_other_phone_idx;
+DROP INDEX IF EXISTS actor.actor_usr_ident_value_idx;
+DROP INDEX IF EXISTS actor.actor_usr_ident_value2_idx;
+
+CREATE INDEX actor_usr_first_given_name_idx ON actor.usr (evergreen.lowercase(first_given_name));
+CREATE INDEX actor_usr_second_given_name_idx ON actor.usr (evergreen.lowercase(second_given_name));
+CREATE INDEX actor_usr_family_name_idx ON actor.usr (evergreen.lowercase(family_name));
+CREATE INDEX actor_usr_email_idx ON actor.usr (evergreen.lowercase(email));
+CREATE INDEX actor_usr_day_phone_idx ON actor.usr (evergreen.lowercase(day_phone));
+CREATE INDEX actor_usr_evening_phone_idx ON actor.usr (evergreen.lowercase(evening_phone));
+CREATE INDEX actor_usr_other_phone_idx ON actor.usr (evergreen.lowercase(other_phone));
+CREATE INDEX actor_usr_ident_value_idx ON actor.usr (evergreen.lowercase(ident_value));
+CREATE INDEX actor_usr_ident_value2_idx ON actor.usr (evergreen.lowercase(ident_value2));
+
+-- update actor.card indexes
+DROP INDEX IF EXISTS actor.actor_card_barcode_evergreen_lowercase_idx;
+CREATE INDEX actor_card_barcode_evergreen_lowercase_idx ON actor.card (evergreen.lowercase(barcode));
+
+CREATE OR REPLACE FUNCTION vandelay.match_bib_record ( ) RETURNS TRIGGER AS $func$
+DECLARE
+    attr        RECORD;
+    attr_def    RECORD;
+    eg_rec      RECORD;
+    id_value    TEXT;
+    exact_id    BIGINT;
+BEGIN
+
+    DELETE FROM vandelay.bib_match WHERE queued_record = NEW.id;
+
+    SELECT * INTO attr_def FROM vandelay.bib_attr_definition WHERE xpath = '//*[@tag="901"]/*[@code="c"]' ORDER BY id LIMIT 1;
+
+    IF attr_def IS NOT NULL AND attr_def.id IS NOT NULL THEN
+        id_value := extract_marc_field('vandelay.queued_bib_record', NEW.id, attr_def.xpath, attr_def.remove);
+    
+        IF id_value IS NOT NULL AND id_value <> '' AND id_value ~ $r$^\d+$$r$ THEN
+            SELECT id INTO exact_id FROM biblio.record_entry WHERE id = id_value::BIGINT AND NOT deleted;
+            SELECT * INTO attr FROM vandelay.queued_bib_record_attr WHERE record = NEW.id and field = attr_def.id LIMIT 1;
+            IF exact_id IS NOT NULL THEN
+                INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, exact_id);
+            END IF;
+        END IF;
+    END IF;
+
+    IF exact_id IS NULL THEN
+        FOR attr IN SELECT a.* FROM vandelay.queued_bib_record_attr a JOIN vandelay.bib_attr_definition d ON (d.id = a.field) WHERE record = NEW.id AND d.ident IS TRUE LOOP
+    
+               -- All numbers? check for an id match
+               IF (attr.attr_value ~ $r$^\d+$$r$) THEN
+               FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE id = attr.attr_value::BIGINT AND deleted IS FALSE LOOP
+                       INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, eg_rec.id);
+                       END LOOP;
+               END IF;
+    
+               -- Looks like an ISBN? check for an isbn match
+               IF (attr.attr_value ~* $r$^[0-9x]+$$r$ AND character_length(attr.attr_value) IN (10,13)) THEN
+               FOR eg_rec IN EXECUTE $$SELECT * FROM metabib.full_rec fr WHERE fr.value LIKE evergreen.lowercase('$$ || attr.attr_value || $$%') AND fr.tag = '020' AND fr.subfield = 'a'$$ LOOP
+                               PERFORM id FROM biblio.record_entry WHERE id = eg_rec.record AND deleted IS FALSE;
+                               IF FOUND THEN
+                               INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('isbn', attr.id, NEW.id, eg_rec.record);
+                               END IF;
+                       END LOOP;
+    
+                       -- subcheck for isbn-as-tcn
+                   FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = 'i' || attr.attr_value AND deleted IS FALSE LOOP
+                           INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
+               END LOOP;
+               END IF;
+    
+               -- check for an OCLC tcn_value match
+               IF (attr.attr_value ~ $r$^o\d+$$r$) THEN
+                   FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = regexp_replace(attr.attr_value,'^o','ocm') AND deleted IS FALSE LOOP
+                           INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
+               END LOOP;
+               END IF;
+    
+               -- check for a direct tcn_value match
+            FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = attr.attr_value AND deleted IS FALSE LOOP
+                INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
+            END LOOP;
+    
+               -- check for a direct item barcode match
+            FOR eg_rec IN
+                    SELECT  DISTINCT b.*
+                      FROM  biblio.record_entry b
+                            JOIN asset.call_number cn ON (cn.record = b.id)
+                            JOIN asset.copy cp ON (cp.call_number = cn.id)
+                      WHERE cp.barcode = attr.attr_value AND cp.deleted IS FALSE
+            LOOP
+                INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, eg_rec.id);
+            END LOOP;
+    
+        END LOOP;
+    END IF;
+
+    RETURN NULL;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+
+-- 0499
+CREATE OR REPLACE FUNCTION asset.label_normalizer_generic(TEXT) RETURNS TEXT AS $func$
+    # Created after looking at the Koha C4::ClassSortRoutine::Generic module,
+    # thus could probably be considered a derived work, although nothing was
+    # directly copied - but to err on the safe side of providing attribution:
+    # Copyright (C) 2007 LibLime
+    # Copyright (C) 2011 Equinox Software, Inc (Steve Callendar)
+    # Licensed under the GPL v2 or later
+
+    use strict;
+    use warnings;
+
+    # Converts the callnumber to uppercase
+    # Strips spaces from start and end of the call number
+    # Converts anything other than letters, digits, and periods into spaces
+    # Collapses multiple spaces into a single underscore
+    my $callnum = uc(shift);
+    $callnum =~ s/^\s//g;
+    $callnum =~ s/\s$//g;
+    # NOTE: this previously used underscores, but this caused sorting issues
+    # for the "before" half of page 0 on CN browse, sorting CNs containing a
+    # decimal before "whole number" CNs
+    $callnum =~ s/[^A-Z0-9_.]/ /g;
+    $callnum =~ s/ {2,}/ /g;
+
+    return $callnum;
+$func$ LANGUAGE PLPERLU;
+
+
+
+-- 0501
+INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('language','Language (2.0 compat version)','Lang');
+UPDATE metabib.record_attr SET attrs = attrs || hstore('language',(attrs->'item_lang'));
+
+-- 0502
+-- Dewey fields
+UPDATE asset.call_number_class
+    SET field = '080ab,082ab,092abef'
+    WHERE id = 2
+;
+
+-- LC fields
+UPDATE asset.call_number_class
+    SET field = '050ab,055ab,090abef'
+    WHERE id = 3
+;
+
+-- FAIR WARNING:
+-- Using a tool such as pgadmin to run this script may fail
+-- If it does, try psql command line.
+
+-- Change this to FALSE to disable updating existing circs
+-- Otherwise will use the fine interval for the grace period
+\set CircGrace TRUE
+
+-- 0503
+-- New Columns
+
+ALTER TABLE config.circ_matrix_matchpoint
+    ADD COLUMN grace_period INTERVAL;
+
+ALTER TABLE config.rule_recurring_fine
+    ADD COLUMN grace_period INTERVAL NOT NULL DEFAULT '1 day';
+
+ALTER TABLE action.circulation
+    ADD COLUMN grace_period INTERVAL NOT NULL DEFAULT '0 seconds';
+
+ALTER TABLE action.aged_circulation
+    ADD COLUMN grace_period INTERVAL NOT NULL DEFAULT '0 seconds';
+
+-- Remove defaults needed to stop null complaints
+
+ALTER TABLE action.circulation
+    ALTER COLUMN grace_period DROP DEFAULT;
+
+ALTER TABLE action.aged_circulation
+    ALTER COLUMN grace_period DROP DEFAULT;
+
+-- Drop Views
+
+DROP VIEW action.all_circulation;
+DROP VIEW action.open_circulation;
+DROP VIEW action.billable_circulations;
+
+-- Replace Views
+
+CREATE OR REPLACE VIEW action.all_circulation AS
+    SELECT  id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
+        copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
+        circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
+        stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
+        max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
+        max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
+      FROM  action.aged_circulation
+            UNION ALL
+    SELECT  DISTINCT circ.id,COALESCE(a.post_code,b.post_code) AS usr_post_code, p.home_ou AS usr_home_ou, p.profile AS usr_profile, EXTRACT(YEAR FROM p.dob)::INT AS usr_birth_year,
+        cp.call_number AS copy_call_number, cp.location AS copy_location, cn.owning_lib AS copy_owning_lib, cp.circ_lib AS copy_circ_lib,
+        cn.record AS copy_bib_record, circ.xact_start, circ.xact_finish, circ.target_copy, circ.circ_lib, circ.circ_staff, circ.checkin_staff,
+        circ.checkin_lib, circ.renewal_remaining, circ.grace_period, circ.due_date, circ.stop_fines_time, circ.checkin_time, circ.create_time, circ.duration,
+        circ.fine_interval, circ.recurring_fine, circ.max_fine, circ.phone_renewal, circ.desk_renewal, circ.opac_renewal, circ.duration_rule,
+        circ.recurring_fine_rule, circ.max_fine_rule, circ.stop_fines, circ.workstation, circ.checkin_workstation, circ.checkin_scan_time,
+        circ.parent_circ
+      FROM  action.circulation circ
+        JOIN asset.copy cp ON (circ.target_copy = cp.id)
+        JOIN asset.call_number cn ON (cp.call_number = cn.id)
+        JOIN actor.usr p ON (circ.usr = p.id)
+        LEFT JOIN actor.usr_address a ON (p.mailing_address = a.id)
+        LEFT JOIN actor.usr_address b ON (p.billing_address = a.id);
+
+CREATE OR REPLACE VIEW action.open_circulation AS
+       SELECT  *
+         FROM  action.circulation
+         WHERE checkin_time IS NULL
+         ORDER BY due_date;
+               
+
+CREATE OR REPLACE VIEW action.billable_circulations AS
+       SELECT  *
+         FROM  action.circulation
+         WHERE xact_finish IS NULL;
+
+-- Drop Functions that rely on types
+
+DROP FUNCTION action.item_user_circ_test(INT, BIGINT, INT, BOOL);
+DROP FUNCTION action.item_user_circ_test(INT, BIGINT, INT);
+DROP FUNCTION action.item_user_renew_test(INT, BIGINT, INT);
+
+CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$
+DECLARE
+    user_object             actor.usr%ROWTYPE;
+    standing_penalty        config.standing_penalty%ROWTYPE;
+    item_object             asset.copy%ROWTYPE;
+    item_status_object      config.copy_status%ROWTYPE;
+    item_location_object    asset.copy_location%ROWTYPE;
+    result                  action.circ_matrix_test_result;
+    circ_test               action.found_circ_matrix_matchpoint;
+    circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
+    out_by_circ_mod         config.circ_matrix_circ_mod_test%ROWTYPE;
+    circ_mod_map            config.circ_matrix_circ_mod_test_map%ROWTYPE;
+    hold_ratio              action.hold_stats%ROWTYPE;
+    penalty_type            TEXT;
+    items_out               INT;
+    context_org_list        INT[];
+    done                    BOOL := FALSE;
+BEGIN
+    -- Assume success unless we hit a failure condition
+    result.success := TRUE;
+
+    -- Fail if the user is BARRED
+    SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
+
+    -- Fail if we couldn't find the user 
+    IF user_object.id IS NULL THEN
+        result.fail_part := 'no_user';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
+
+    -- Fail if we couldn't find the item 
+    IF item_object.id IS NULL THEN
+        result.fail_part := 'no_item';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    IF user_object.barred IS TRUE THEN
+        result.fail_part := 'actor.usr.barred';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the item can't circulate
+    IF item_object.circulate IS FALSE THEN
+        result.fail_part := 'asset.copy.circulate';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the item isn't in a circulateable status on a non-renewal
+    IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
+        result.fail_part := 'asset.copy.status';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    ELSIF renewal AND item_object.status <> 1 THEN
+        result.fail_part := 'asset.copy.status';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the item can't circulate because of the shelving location
+    SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
+    IF item_location_object.circulate IS FALSE THEN
+        result.fail_part := 'asset.copy_location.circulate';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
+
+    circ_matchpoint             := circ_test.matchpoint;
+    result.matchpoint           := circ_matchpoint.id;
+    result.circulate            := circ_matchpoint.circulate;
+    result.duration_rule        := circ_matchpoint.duration_rule;
+    result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
+    result.max_fine_rule        := circ_matchpoint.max_fine_rule;
+    result.hard_due_date        := circ_matchpoint.hard_due_date;
+    result.renewals             := circ_matchpoint.renewals;
+    result.grace_period         := circ_matchpoint.grace_period;
+    result.buildrows            := circ_test.buildrows;
+
+    -- Fail if we couldn't find a matchpoint
+    IF circ_test.success = false THEN
+        result.fail_part := 'no_matchpoint';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN; -- All tests after this point require a matchpoint. No sense in running on an incomplete or missing one.
+    END IF;
+
+    -- Apparently....use the circ matchpoint org unit to determine what org units are valid.
+    SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( circ_matchpoint.org_unit );
+
+    IF renewal THEN
+        penalty_type = '%RENEW%';
+    ELSE
+        penalty_type = '%CIRC%';
+    END IF;
+
+    FOR standing_penalty IN
+        SELECT  DISTINCT csp.*
+          FROM  actor.usr_standing_penalty usp
+                JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
+          WHERE usr = match_user
+                AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
+                AND (usp.stop_date IS NULL or usp.stop_date > NOW())
+                AND csp.block_list LIKE penalty_type LOOP
+
+        result.fail_part := standing_penalty.name;
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END LOOP;
+
+    -- Fail if the test is set to hard non-circulating
+    IF circ_matchpoint.circulate IS FALSE THEN
+        result.fail_part := 'config.circ_matrix_test.circulate';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the total copy-hold ratio is too low
+    IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
+        SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
+        IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
+            result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END IF;
+
+    -- Fail if the available copy-hold ratio is too low
+    IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
+        IF hold_ratio.hold_count IS NULL THEN
+            SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
+        END IF;
+        IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
+            result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END IF;
+
+    -- Fail if the user has too many items with specific circ_modifiers checked out
+    FOR out_by_circ_mod IN SELECT * FROM config.circ_matrix_circ_mod_test WHERE matchpoint = circ_matchpoint.id LOOP
+        SELECT  INTO items_out COUNT(*)
+          FROM  action.circulation circ
+            JOIN asset.copy cp ON (cp.id = circ.target_copy)
+          WHERE circ.usr = match_user
+               AND circ.circ_lib IN ( SELECT * FROM unnest(context_org_list) )
+            AND circ.checkin_time IS NULL
+            AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
+            AND cp.circ_modifier IN (SELECT circ_mod FROM config.circ_matrix_circ_mod_test_map WHERE circ_mod_test = out_by_circ_mod.id);
+        IF items_out >= out_by_circ_mod.items_out THEN
+            result.fail_part := 'config.circ_matrix_circ_mod_test';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END LOOP;
+
+    -- If we passed everything, return the successful matchpoint id
+    IF NOT done THEN
+        RETURN NEXT result;
+    END IF;
+
+    RETURN;
+END;
+$func$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION action.item_user_circ_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
+    SELECT * FROM action.item_user_circ_test( $1, $2, $3, FALSE );
+$func$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION action.item_user_renew_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
+    SELECT * FROM action.item_user_circ_test( $1, $2, $3, TRUE );
+$func$ LANGUAGE SQL;
+
+-- Update recurring fine rules
+UPDATE config.rule_recurring_fine SET grace_period=recurrence_interval;
+
+-- Update Circulation Data
+-- Only update if we were told to and the circ hasn't been checked in
+UPDATE action.circulation SET grace_period=fine_interval WHERE :CircGrace AND (checkin_time IS NULL);
+
+-- 0504
+CREATE TABLE biblio.monograph_part (
+    id              SERIAL  PRIMARY KEY,
+    record          BIGINT  NOT NULL REFERENCES biblio.record_entry (id),
+    label           TEXT    NOT NULL,
+    label_sortkey   TEXT    NOT NULL,
+    CONSTRAINT record_label_unique UNIQUE (record,label)
+);
+
+CREATE OR REPLACE FUNCTION biblio.normalize_biblio_monograph_part_sortkey () RETURNS TRIGGER AS $$
+BEGIN
+    NEW.label_sortkey := REGEXP_REPLACE(
+        evergreen.lpad_number_substrings(
+            naco_normalize(NEW.label),
+            '0',
+            10
+        ),
+        E'\\s+',
+        '',
+        'g'
+    );
+    RETURN NEW;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE TRIGGER norm_sort_label BEFORE INSERT OR UPDATE ON biblio.monograph_part FOR EACH ROW EXECUTE PROCEDURE biblio.normalize_biblio_monograph_part_sortkey();
+
+CREATE TABLE asset.copy_part_map (
+    id          SERIAL  PRIMARY KEY,
+    target_copy BIGINT  NOT NULL, -- points o asset.copy
+    part        INT     NOT NULL REFERENCES biblio.monograph_part (id) ON DELETE CASCADE
+);
+CREATE UNIQUE INDEX copy_part_map_cp_part_idx ON asset.copy_part_map (target_copy, part);
+
+CREATE TABLE asset.call_number_prefix (
+       id                      SERIAL   PRIMARY KEY,
+       owning_lib          INT                 NOT NULL REFERENCES actor.org_unit (id),
+       label               TEXT                NOT NULL, -- i18n
+       label_sortkey   TEXT
+);
+
+CREATE OR REPLACE FUNCTION asset.normalize_affix_sortkey () RETURNS TRIGGER AS $$
+BEGIN
+    NEW.label_sortkey := REGEXP_REPLACE(
+        evergreen.lpad_number_substrings(
+            naco_normalize(NEW.label),
+            '0',
+            10
+        ),
+        E'\\s+',
+        '',
+        'g'
+    );
+    RETURN NEW;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE TRIGGER prefix_normalize_tgr BEFORE INSERT OR UPDATE ON asset.call_number_prefix FOR EACH ROW EXECUTE PROCEDURE asset.normalize_affix_sortkey();
+CREATE UNIQUE INDEX asset_call_number_prefix_once_per_lib ON asset.call_number_prefix (label, owning_lib);
+CREATE INDEX asset_call_number_prefix_sortkey_idx ON asset.call_number_prefix (label_sortkey);
+
+CREATE TABLE asset.call_number_suffix (
+       id                      SERIAL   PRIMARY KEY,
+       owning_lib          INT                 NOT NULL REFERENCES actor.org_unit (id),
+       label               TEXT                NOT NULL, -- i18n
+       label_sortkey   TEXT
+);
+CREATE TRIGGER suffix_normalize_tgr BEFORE INSERT OR UPDATE ON asset.call_number_suffix FOR EACH ROW EXECUTE PROCEDURE asset.normalize_affix_sortkey();
+CREATE UNIQUE INDEX asset_call_number_suffix_once_per_lib ON asset.call_number_suffix (label, owning_lib);
+CREATE INDEX asset_call_number_suffix_sortkey_idx ON asset.call_number_suffix (label_sortkey);
+
+INSERT INTO asset.call_number_suffix (id, owning_lib, label) VALUES (-1, 1, '');
+INSERT INTO asset.call_number_prefix (id, owning_lib, label) VALUES (-1, 1, '');
+
+DROP INDEX IF EXISTS asset.asset_call_number_label_once_per_lib;
+
+ALTER TABLE asset.call_number
+    ADD COLUMN prefix INT NOT NULL DEFAULT -1 REFERENCES asset.call_number_prefix(id) DEFERRABLE INITIALLY DEFERRED,
+    ADD COLUMN suffix INT NOT NULL DEFAULT -1 REFERENCES asset.call_number_suffix(id) DEFERRABLE INITIALLY DEFERRED;
+
+ALTER TABLE auditor.asset_call_number_history
+    ADD COLUMN prefix INT NOT NULL DEFAULT -1,
+    ADD COLUMN suffix INT NOT NULL DEFAULT -1;
+
+CREATE UNIQUE INDEX asset_call_number_label_once_per_lib ON asset.call_number (record, owning_lib, label, prefix, suffix) WHERE deleted = FALSE OR deleted IS FALSE;
+
+INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
+    'ui.cat.volume_copy_editor.horizontal',
+    oils_i18n_gettext(
+        'ui.cat.volume_copy_editor.horizontal',
+        'GUI: Horizontal layout for Volume/Copy Creator/Editor.',
+        'coust', 'label'),
+    oils_i18n_gettext(
+        'ui.cat.volume_copy_editor.horizontal',
+        'The main entry point for this interface is in Holdings Maintenance, Actions for Selected Rows, Edit Item Attributes / Call Numbers / Replace Barcodes.  This setting changes the top and bottom panes for that interface into left and right panes.',
+       'coust', 'description'),
+    'bool'
+);
+
+
+
+-- 0506
+ALTER FUNCTION actor.org_unit_descendants( INT, INT ) ROWS 1;
+ALTER FUNCTION actor.org_unit_descendants( INT ) ROWS 1;
+ALTER FUNCTION actor.org_unit_descendants_distance( INT )  ROWS 1;
+ALTER FUNCTION actor.org_unit_ancestors( INT )  ROWS 1;
+ALTER FUNCTION actor.org_unit_ancestors_distance( INT )  ROWS 1;
+ALTER FUNCTION actor.org_unit_full_path ( INT )  ROWS 2;
+ALTER FUNCTION actor.org_unit_full_path ( INT, INT ) ROWS 2;
+ALTER FUNCTION actor.org_unit_combined_ancestors ( INT, INT ) ROWS 1;
+ALTER FUNCTION actor.org_unit_common_ancestors ( INT, INT ) ROWS 1;
+ALTER FUNCTION actor.org_unit_ancestor_setting( TEXT, INT ) ROWS 1;
+ALTER FUNCTION permission.grp_ancestors ( INT ) ROWS 1;
+ALTER FUNCTION permission.grp_ancestors_distance( INT ) ROWS 1;
+ALTER FUNCTION permission.grp_descendants_distance( INT ) ROWS 1;
+ALTER FUNCTION permission.usr_perms ( INT ) ROWS 10;
+ALTER FUNCTION permission.usr_has_perm_at_nd ( INT, TEXT) ROWS 1;
+ALTER FUNCTION permission.usr_has_perm_at_all_nd ( INT, TEXT ) ROWS 1;
+ALTER FUNCTION permission.usr_has_perm_at ( INT, TEXT ) ROWS 1;
+ALTER FUNCTION permission.usr_has_perm_at_all ( INT, TEXT ) ROWS 1;
+
+
+CREATE TRIGGER facet_force_nfc_tgr
+    BEFORE UPDATE OR INSERT ON metabib.facet_entry
+    FOR EACH ROW EXECUTE PROCEDURE evergreen.facet_force_nfc();
+
+DROP FUNCTION IF EXISTS public.force_unicode_normal_form (TEXT,TEXT);
+DROP FUNCTION IF EXISTS public.facet_force_nfc ();
+
+DROP TRIGGER b_maintain_901 ON biblio.record_entry;
+DROP TRIGGER b_maintain_901 ON authority.record_entry;
+DROP TRIGGER b_maintain_901 ON serial.record_entry;
+
+CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON biblio.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
+CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON authority.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
+CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON serial.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
+
+DROP FUNCTION IF EXISTS public.maintain_901 ();
+
+------ Backporting note: 2.1+ only beyond here --------
+
+CREATE SCHEMA unapi;
+
+CREATE TABLE unapi.bre_output_layout (
+    name                TEXT    PRIMARY KEY,
+    transform           TEXT    REFERENCES config.xml_transform (name) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    mime_type           TEXT    NOT NULL,
+    feed_top            TEXT    NOT NULL,
+    holdings_element    TEXT,
+    title_element       TEXT,
+    description_element TEXT,
+    creator_element     TEXT,
+    update_ts_element   TEXT
+);
+
+INSERT INTO unapi.bre_output_layout
+    (name,           transform, mime_type,              holdings_element, feed_top,         title_element, description_element, creator_element, update_ts_element)
+        VALUES
+    ('holdings_xml', NULL,      'application/xml',      NULL,             'hxml',           NULL,          NULL,                NULL,            NULL),
+    ('marcxml',      'marcxml', 'application/marc+xml', 'record',         'collection',     NULL,          NULL,                NULL,            NULL),
+    ('mods32',       'mods32',  'application/mods+xml', 'mods',           'modsCollection', NULL,          NULL,                NULL,            NULL)
+;
+
+-- Dummy functions, so we can create the real ones out of order
+CREATE OR REPLACE FUNCTION unapi.aou    ( 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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.acnp   ( 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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.acns   ( 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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.acn    ( 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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.ssub   ( 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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.sdist  ( 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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.sstr   ( 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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.sitem  ( 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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.sunit  ( 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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.sisum  ( 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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.sbsum  ( 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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.sssum  ( 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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.siss   ( 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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.auri   ( 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 NULL::XML $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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.acpn   ( 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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.acl    ( 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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.ccs    ( 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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.ascecm ( 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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.bre    ( 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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.bmp    ( 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 NULL::XML $F$ LANGUAGE SQL;
+
+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 NULL::XML $F$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION unapi.biblio_record_entry_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, title TEXT DEFAULT NULL, description TEXT DEFAULT NULL, creator TEXT DEFAULT NULL, update_ts TEXT DEFAULT NULL, unapi_url TEXT DEFAULT NULL, header_xml XML DEFAULT NULL ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.memoize (classname TEXT, 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$
+DECLARE
+    key     TEXT;
+    output  XML;
+BEGIN
+    key :=
+        'id'        || COALESCE(obj_id::TEXT,'') ||
+        'format'    || COALESCE(format::TEXT,'') ||
+        'ename'     || COALESCE(ename::TEXT,'') ||
+        'includes'  || COALESCE(includes::TEXT,'{}'::TEXT[]::TEXT) ||
+        'org'       || COALESCE(org::TEXT,'') ||
+        'depth'     || COALESCE(depth::TEXT,'') ||
+        'slimit'    || COALESCE(slimit::TEXT,'') ||
+        'soffset'   || COALESCE(soffset::TEXT,'') ||
+        'include_xmlns'   || COALESCE(include_xmlns::TEXT,'');
+    -- RAISE NOTICE 'memoize key: %', key;
+
+    key := MD5(key);
+    -- RAISE NOTICE 'memoize hash: %', key;
+
+    -- XXX cache logic ... memcached? table?
+
+    EXECUTE $$SELECT unapi.$$ || classname || $$( $1, $2, $3, $4, $5, $6, $7, $8, $9);$$ INTO output USING obj_id, format, ename, includes, org, depth, slimit, soffset, include_xmlns;
+    RETURN output;
+END;
+$F$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION unapi.biblio_record_entry_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, title TEXT DEFAULT NULL, description TEXT DEFAULT NULL, creator TEXT DEFAULT NULL, update_ts TEXT DEFAULT NULL, unapi_url TEXT DEFAULT NULL, header_xml XML DEFAULT NULL ) RETURNS XML AS $F$
+DECLARE
+    layout          unapi.bre_output_layout%ROWTYPE;
+    transform       config.xml_transform%ROWTYPE;
+    item_format     TEXT;
+    tmp_xml         TEXT;
+    xmlns_uri       TEXT := 'http://open-ils.org/spec/feed-xml/v1';
+    ouid            INT;
+    element_list    TEXT[];
+BEGIN
+
+    SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+    SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+    IF layout.name IS NULL THEN
+        RETURN NULL::XML;
+    END IF;
+
+    SELECT * INTO transform FROM config.xml_transform WHERE name = layout.transform;
+    xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri);
+
+    -- Gather the bib xml
+    SELECT XMLAGG( unapi.bre(i, format, '', includes, org, depth, slimit, soffset, include_xmlns)) INTO tmp_xml FROM UNNEST( id_list ) i;
+
+    IF layout.title_element IS NOT NULL THEN
+        EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title, include_xmlns;
+    END IF;
+
+    IF layout.description_element IS NOT NULL THEN
+        EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.description_element ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, description, include_xmlns;
+    END IF;
+
+    IF layout.creator_element IS NOT NULL THEN
+        EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.creator_element ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, creator, include_xmlns;
+    END IF;
+
+    IF layout.update_ts_element IS NOT NULL THEN
+        EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.update_ts_element ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, update_ts, include_xmlns;
+    END IF;
+
+    IF unapi_url IS NOT NULL THEN
+        EXECUTE $$SELECT XMLCONCAT( XMLELEMENT( name link, XMLATTRIBUTES( 'http://www.w3.org/1999/xhtml' AS xmlns, 'unapi-server' AS rel, $1 AS href, 'unapi' AS title)), $2)$$ INTO tmp_xml USING unapi_url, tmp_xml::XML;
+    END IF;
+
+    IF header_xml IS NOT NULL THEN tmp_xml := XMLCONCAT(header_xml,tmp_xml::XML); END IF;
+
+    element_list := regexp_split_to_array(layout.feed_top,E'\\.');
+    FOR i IN REVERSE ARRAY_UPPER(element_list, 1) .. 1 LOOP
+        EXECUTE 'SELECT XMLELEMENT( name '|| quote_ident(element_list[i]) ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, include_xmlns;
+    END LOOP;
+
+    RETURN tmp_xml::XML;
+END;
+$F$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION unapi.bre ( 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$
+DECLARE
+    me      biblio.record_entry%ROWTYPE;
+    layout  unapi.bre_output_layout%ROWTYPE;
+    xfrm    config.xml_transform%ROWTYPE;
+    ouid    INT;
+    tmp_xml TEXT;
+    top_el  TEXT;
+    output  XML;
+    hxml    XML;
+BEGIN
+
+    SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+
+    IF ouid IS NULL THEN
+        RETURN NULL::XML;
+    END IF;
+
+    IF format = 'holdings_xml' THEN -- the special case
+        output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns);
+        RETURN output;
+    END IF;
+
+    SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+    IF layout.name IS NULL THEN
+        RETURN NULL::XML;
+    END IF;
+
+    SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
+
+    SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
+
+    -- grab hodlings if we need them
+    IF ('holdings_xml' = ANY (includes)) THEN 
+        hxml := unapi.holdings_xml(obj_id, ouid, org, depth, evergreen.array_remove_item_by_value(includes,'holdings_xml'), slimit, soffset, include_xmlns);
+    ELSE
+        hxml := NULL::XML;
+    END IF;
+
+
+    -- generate our item node
+
+
+    IF format = 'marcxml' THEN
+        tmp_xml := me.marc;
+        IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
+           tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
+        END IF; 
+    ELSE
+        tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
+    END IF;
+
+    top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
+
+    IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
+        tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
+    END IF;
+
+    IF ('bre.unapi' = ANY (includes)) THEN 
+        output := REGEXP_REPLACE(
+            tmp_xml,
+            '</' || top_el || '>(.*?)',
+            XMLELEMENT(
+                name abbr,
+                XMLATTRIBUTES(
+                    'http://www.w3.org/1999/xhtml' AS xmlns,
+                    'unapi-id' AS class,
+                    'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
+                )
+            )::TEXT || '</' || top_el || E'>\\1'
+        );
+    ELSE
+        output := tmp_xml;
+    END IF;
+
+    RETURN output;
+END;
+$F$ 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
+             );
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.ssub ( 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 subscription,
+                    XMLATTRIBUTES(
+                        CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                        'tag:open-ils.org:U2@ssub/' || id AS id,
+                        start_date AS start, end_date AS end, expected_date_offset
+                    ),
+                    unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8),
+                    XMLELEMENT( name distributions,
+                        CASE 
+                            WHEN ('sdist' = ANY ($4)) THEN
+                                XMLAGG((SELECT unapi.sdist( id, 'xml', 'distribution', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8, FALSE) FROM serial.distribution WHERE subscription = ssub.id))
+                            ELSE NULL
+                        END
+                    )
+                )
+          FROM  serial.subscription ssub
+          WHERE id = $1
+          GROUP BY id, start_date, end_date, expected_date_offset, owning_lib;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.sdist ( 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 distribution,
+                    XMLATTRIBUTES(
+                        CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                        'tag:open-ils.org:U2@sdist/' || id AS id,
+                       'tag:open-ils.org:U2@acn/' || receive_call_number AS receive_call_number,
+                       'tag:open-ils.org:U2@acn/' || bind_call_number AS bind_call_number,
+                        unit_label_prefix, label, unit_label_suffix, summary_method
+                    ),
+                    unapi.aou( holding_lib, $2, 'holding_lib', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8),
+                    CASE WHEN subscription IS NOT NULL AND ('ssub' = ANY ($4)) THEN unapi.ssub( subscription, 'xml', 'subscription', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) ELSE NULL END,
+                    XMLELEMENT( name streams,
+                        CASE 
+                            WHEN ('sstr' = ANY ($4)) THEN
+                                XMLAGG((SELECT unapi.sstr( id, 'xml', 'stream', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.stream WHERE distribution = sdist.id))
+                            ELSE NULL
+                        END
+                    ),
+                    XMLELEMENT( name summaries,
+                        CASE 
+                            WHEN ('ssum' = ANY ($4)) THEN
+                                XMLAGG((SELECT unapi.sbsum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.basic_summary WHERE distribution = sdist.id))
+                            ELSE NULL
+                        END,
+                        CASE 
+                            WHEN ('ssum' = ANY ($4)) THEN
+                                XMLAGG((SELECT unapi.sisum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.index_summary WHERE distribution = sdist.id))
+                            ELSE NULL
+                        END,
+                        CASE 
+                            WHEN ('ssum' = ANY ($4)) THEN
+                                XMLAGG((SELECT unapi.sssum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.supplement_summary WHERE distribution = sdist.id))
+                            ELSE NULL
+                        END
+                    )
+                )
+          FROM  serial.distribution sdist
+          WHERE id = $1
+          GROUP BY id, label, unit_label_prefix, unit_label_suffix, holding_lib, summary_method, subscription, receive_call_number, bind_call_number;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.sstr ( 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 stream,
+                XMLATTRIBUTES(
+                    CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                    'tag:open-ils.org:U2@sstr/' || id AS id,
+                    routing_label
+                ),
+                CASE WHEN distribution IS NOT NULL AND ('sdist' = ANY ($4)) THEN unapi.sssum( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'sstr'), $5, $6, $7, $8, FALSE) ELSE NULL END,
+                XMLELEMENT( name items,
+                    CASE 
+                        WHEN ('sitem' = ANY ($4)) THEN
+                            XMLAGG((SELECT unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'sstr'), $5, $6, $7, $8, FALSE) FROM serial.item WHERE stream = sstr.id))
+                        ELSE NULL
+                    END
+                )
+            )
+      FROM  serial.stream sstr
+      WHERE id = $1
+      GROUP BY id, routing_label, distribution;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.siss ( 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 issuance,
+                XMLATTRIBUTES(
+                    CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                    'tag:open-ils.org:U2@siss/' || id AS id,
+                    create_date, edit_date, label, date_published,
+                    holding_code, holding_type, holding_link_id
+                ),
+                CASE WHEN subscription IS NOT NULL AND ('ssub' = ANY ($4)) THEN unapi.ssub( subscription, 'xml', 'subscription', evergreen.array_remove_item_by_value($4,'siss'), $5, $6, $7, $8, FALSE) ELSE NULL END,
+                XMLELEMENT( name items,
+                    CASE 
+                        WHEN ('sitem' = ANY ($4)) THEN
+                            XMLAGG((SELECT unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'siss'), $5, $6, $7, $8, FALSE) FROM serial.item WHERE issuance = sstr.id))
+                        ELSE NULL
+                    END
+                )
+            )
+      FROM  serial.issuance sstr
+      WHERE id = $1
+      GROUP BY id, create_date, edit_date, label, date_published, holding_code, holding_type, holding_link_id, subscription;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.sitem ( 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 serial_item,
+                    XMLATTRIBUTES(
+                        CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                        'tag:open-ils.org:U2@sitem/' || id AS id,
+                        'tag:open-ils.org:U2@siss/' || issuance AS issuance,
+                        date_expected, date_received
+                    ),
+                    CASE WHEN issuance IS NOT NULL AND ('siss' = ANY ($4)) THEN unapi.siss( issuance, $2, 'issuance', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
+                    CASE WHEN stream IS NOT NULL AND ('sstr' = ANY ($4)) THEN unapi.sstr( stream, $2, 'stream', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
+                    CASE WHEN unit IS NOT NULL AND ('sunit' = ANY ($4)) THEN unapi.sunit( stream, $2, 'serial_unit', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
+                    CASE WHEN uri IS NOT NULL AND ('auri' = ANY ($4)) THEN unapi.auri( uri, $2, 'uri', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END
+--                    XMLELEMENT( name 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) FROM asset.copy_note WHERE owning_copy = cp.id AND pub))
+--                            ELSE NULL
+--                        END
+--                    )
+                )
+          FROM  serial.item sitem
+          WHERE id = $1;
+$F$ LANGUAGE SQL;
+
+
+CREATE OR REPLACE FUNCTION unapi.sssum ( 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 serial_summary,
+                XMLATTRIBUTES(
+                    CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                    'tag:open-ils.org:U2@sbsum/' || id AS id,
+                    'sssum' AS type, generated_coverage, textual_holdings, show_generated
+                ),
+                CASE WHEN ('sdist' = ANY ($4)) THEN unapi.sdist( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'ssum'), $5, $6, $7, $8, FALSE) ELSE NULL END
+            )
+      FROM  serial.supplement_summary ssum
+      WHERE id = $1
+      GROUP BY id, generated_coverage, textual_holdings, distribution, show_generated;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.sbsum ( 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 serial_summary,
+                XMLATTRIBUTES(
+                    CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                    'tag:open-ils.org:U2@sbsum/' || id AS id,
+                    'sbsum' AS type, generated_coverage, textual_holdings, show_generated
+                ),
+                CASE WHEN ('sdist' = ANY ($4)) THEN unapi.sdist( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'ssum'), $5, $6, $7, $8, FALSE) ELSE NULL END
+            )
+      FROM  serial.basic_summary ssum
+      WHERE id = $1
+      GROUP BY id, generated_coverage, textual_holdings, distribution, show_generated;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.sisum ( 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 serial_summary,
+                XMLATTRIBUTES(
+                    CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                    'tag:open-ils.org:U2@sbsum/' || id AS id,
+                    'sisum' AS type, generated_coverage, textual_holdings, show_generated
+                ),
+                CASE WHEN ('sdist' = ANY ($4)) THEN unapi.sdist( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'ssum'), $5, $6, $7, $8, FALSE) ELSE NULL END
+            )
+      FROM  serial.index_summary ssum
+      WHERE id = $1
+      GROUP BY id, generated_coverage, textual_holdings, distribution, show_generated;
+$F$ LANGUAGE SQL;
+
+
+CREATE OR REPLACE FUNCTION unapi.aou ( 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$
+DECLARE
+    output XML;
+BEGIN
+    IF ename = 'circlib' THEN
+        SELECT  XMLELEMENT(
+                    name circlib,
+                    XMLATTRIBUTES(
+                        'http://open-ils.org/spec/actors/v1' AS xmlns,
+                        id AS ident
+                    ),
+                    name
+                ) INTO output
+          FROM  actor.org_unit aou
+          WHERE id = obj_id;
+    ELSE
+        EXECUTE $$SELECT  XMLELEMENT(
+                    name $$ || ename || $$,
+                    XMLATTRIBUTES(
+                        'http://open-ils.org/spec/actors/v1' AS xmlns,
+                        'tag:open-ils.org:U2@aou/' || id AS id,
+                        shortname, name, opac_visible
+                    )
+                )
+          FROM  actor.org_unit aou
+         WHERE id = $1 $$ INTO output USING obj_id;
+    END IF;
+
+    RETURN output;
+
+END;
+$F$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION unapi.acl ( 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 location,
+                XMLATTRIBUTES(
+                    CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                    id AS ident
+                ),
+                name
+            )
+      FROM  asset.copy_location
+      WHERE id = $1;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.ccs ( 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 status,
+                XMLATTRIBUTES(
+                    CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                    id AS ident
+                ),
+                name
+            )
+      FROM  config.copy_status
+      WHERE id = $1;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.acpn ( 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_note,
+                    XMLATTRIBUTES(
+                        CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                        create_date AS date,
+                        title
+                    ),
+                    value
+                )
+          FROM  asset.copy_note
+          WHERE id = $1;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.ascecm ( 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 statcat,
+                    XMLATTRIBUTES(
+                        CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                        sc.name,
+                        sc.opac_visible
+                    ),
+                    asce.value
+                )
+          FROM  asset.stat_cat_entry asce
+                JOIN asset.stat_cat sc ON (sc.id = asce.stat_cat)
+          WHERE asce.id = $1;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.bmp ( 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 monograph_part,
+                    XMLATTRIBUTES(
+                        CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                        'tag:open-ils.org:U2@bmp/' || id AS id,
+                        id AS ident,
+                        label,
+                        label_sortkey,
+                        'tag:open-ils.org:U2@bre/' || record AS record
+                    ),
+                    CASE 
+                        WHEN ('acp' = ANY ($4)) THEN
+                            XMLELEMENT( name copies,
+                                (SELECT XMLAGG(acp) FROM (
+                                    SELECT  unapi.acp( cp.id, 'xml', 'copy', evergreen.array_remove_item_by_value($4,'bmp'), $5, $6, $7, $8, FALSE)
+                                      FROM  asset.copy cp
+                                            JOIN asset.copy_part_map cpm ON (cpm.target_copy = cp.id)
+                                      WHERE cpm.part = $1
+                                      ORDER BY COALESCE(cp.copy_number,0), cp.barcode
+                                      LIMIT $7
+                                      OFFSET $8
+                                )x)
+                            )
+                        ELSE NULL
+                    END,
+                    CASE WHEN ('bre' = ANY ($4)) THEN unapi.bre( record, 'marcxml', 'record', evergreen.array_remove_item_by_value($4,'bmp'), $5, $6, $7, $8, FALSE) ELSE NULL END
+                )
+          FROM  biblio.monograph_part
+          WHERE id = $1
+          GROUP BY id, label, label_sortkey, record;
+$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
+                    ),
+                    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 unapi.sunit ( 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 serial_unit,
+                    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, status_changed_time,
+                        floating, mint_condition, detailed_contents, sort_key, summary_contents, cost 
+                    ),
+                    unapi.ccs( status, $2, 'status', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE),
+                    unapi.acl( location, $2, 'location', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE),
+                    unapi.aou( circ_lib, $2, 'circ_lib', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8),
+                    unapi.aou( circ_lib, $2, 'circlib', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $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( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $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.acpn( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE) FROM asset.stat_cat_entry_copy_map WHERE owning_copy = cp.id))
+                            ELSE NULL
+                        END
+                    )
+                )
+          FROM  serial.unit cp
+          WHERE id = $1
+          GROUP BY  id, status, location, circ_lib, call_number, create_date, edit_date, copy_number, circulate, floating, mint_condition,
+                    deposit, ref, holdable, deleted, deposit_amount, price, barcode, circ_modifier, circ_as_type, opac_visible, status_changed_time, detailed_contents, sort_key, summary_contents, cost;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.acn ( 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 volume,
+                    XMLATTRIBUTES(
+                        CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                        'tag:open-ils.org:U2@acn/' || acn.id AS id,
+                        o.shortname AS lib,
+                        o.opac_visible AS opac_visible,
+                        deleted, label, label_sortkey, label_class, record
+                    ),
+                    unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8),
+                    XMLELEMENT( name copies,
+                        CASE 
+                            WHEN ('acp' = ANY ($4)) THEN
+                                (SELECT XMLAGG(acp) FROM (
+                                    SELECT  unapi.acp( cp.id, 'xml', 'copy', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8, FALSE)
+                                      FROM  asset.copy cp
+                                            JOIN actor.org_unit_descendants(
+                                                (SELECT id FROM actor.org_unit WHERE shortname = $5),
+                                                (COALESCE($6,(SELECT aout.depth FROM actor.org_unit_type aout JOIN actor.org_unit aou ON (aou.ou_type = aout.id AND aou.shortname = $5))))
+                                            ) aoud ON (cp.circ_lib = aoud.id)
+                                      WHERE cp.call_number = acn.id
+                                      ORDER BY COALESCE(cp.copy_number,0), cp.barcode
+                                      LIMIT $7
+                                      OFFSET $8
+                                )x)
+                            ELSE NULL
+                        END
+                    ),
+                    XMLELEMENT(
+                        name uris,
+                        (SELECT XMLAGG(auri) FROM (SELECT unapi.auri(uri,'xml','uri', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8, FALSE) FROM asset.uri_call_number_map WHERE call_number = acn.id)x)
+                    ),
+                    CASE WHEN ('acnp' = ANY ($4)) THEN unapi.acnp( acn.prefix, 'marcxml', 'prefix', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8, FALSE) ELSE NULL END,
+                    CASE WHEN ('acns' = ANY ($4)) THEN unapi.acns( acn.suffix, 'marcxml', 'suffix', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8, FALSE) ELSE NULL END,
+                    CASE WHEN ('bre' = ANY ($4)) THEN unapi.bre( acn.record, 'marcxml', 'record', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8, FALSE) ELSE NULL END
+                ) AS x
+          FROM  asset.call_number acn
+                JOIN actor.org_unit o ON (o.id = acn.owning_lib)
+          WHERE acn.id = $1
+          GROUP BY acn.id, o.shortname, o.opac_visible, deleted, label, label_sortkey, label_class, owning_lib, record, acn.prefix, acn.suffix;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.acnp ( 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 call_number_prefix,
+                    XMLATTRIBUTES(
+                        CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                        id AS ident,
+                        label,
+                        label_sortkey
+                    ),
+                    unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'acnp'), $5, $6, $7, $8)
+                )
+          FROM  asset.call_number_prefix
+          WHERE id = $1;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.acns ( 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 call_number_suffix,
+                    XMLATTRIBUTES(
+                        CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                        id AS ident,
+                        label,
+                        label_sortkey
+                    ),
+                    unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'acns'), $5, $6, $7, $8)
+                )
+          FROM  asset.call_number_suffix
+          WHERE id = $1;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.auri ( 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 volume,
+                    XMLATTRIBUTES(
+                        CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                        'tag:open-ils.org:U2@auri/' || uri.id AS id,
+                        use_restriction,
+                        href,
+                        label
+                    ),
+                    XMLELEMENT( name copies,
+                        CASE 
+                            WHEN ('acn' = ANY ($4)) THEN
+                                (SELECT XMLAGG(acn) FROM (SELECT unapi.acn( call_number, 'xml', 'copy', evergreen.array_remove_item_by_value($4,'auri'), $5, $6, $7, $8, FALSE) FROM asset.uri_call_number_map WHERE uri = uri.id)x)
+                            ELSE NULL
+                        END
+                    )
+                ) AS x
+          FROM  asset.uri uri
+          WHERE uri.id = $1
+          GROUP BY uri.id, use_restriction, href, label;
+$F$ LANGUAGE SQL;
+
+DROP FUNCTION IF EXISTS public.array_remove_item_by_value(ANYARRAY,ANYELEMENT);
+
+DROP FUNCTION IF EXISTS public.lpad_number_substrings(TEXT,TEXT,INT);
+
+
+-- 0511
+CREATE OR REPLACE FUNCTION evergreen.fake_fkey_tgr () RETURNS TRIGGER AS $F$
+DECLARE
+    copy_id BIGINT;
+BEGIN
+    EXECUTE 'SELECT ($1).' || quote_ident(TG_ARGV[0]) INTO copy_id USING NEW;
+    PERFORM * FROM asset.copy WHERE id = copy_id;
+    IF NOT FOUND THEN
+        RAISE EXCEPTION 'Key (%.%=%) does not exist in asset.copy', TG_TABLE_SCHEMA, TG_TABLE_NAME, copy_id;
+    END IF;
+    RETURN NULL;
+END;
+$F$ LANGUAGE PLPGSQL;
+
+CREATE TRIGGER action_circulation_target_copy_trig AFTER INSERT OR UPDATE ON action.circulation FOR EACH ROW EXECUTE PROCEDURE evergreen.fake_fkey_tgr('target_copy');
+
+-- 0512
+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 copy_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 copy_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;
+
+-- 0513
+CREATE OR REPLACE FUNCTION unapi.mra ( 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 attributes,
+                    XMLATTRIBUTES(
+                        CASE WHEN $9 THEN 'http://open-ils.org/spec/indexing/v1' ELSE NULL END AS xmlns,
+                        'tag:open-ils.org:U2@mra/' || mra.id AS id,
+                        'tag:open-ils.org:U2@bre/' || mra.id AS record
+                    ),
+                    (SELECT XMLAGG(foo.y)
+                      FROM (SELECT XMLELEMENT(
+                                name field,
+                                XMLATTRIBUTES(
+                                    key AS name,
+                                    cvm.value AS "coded-value",
+                                    rad.filter,
+                                    rad.sorter
+                                ),
+                                x.value
+                            )
+                           FROM EACH(mra.attrs) AS x
+                                JOIN config.record_attr_definition rad ON (x.key = rad.name)
+                                LEFT JOIN config.coded_value_map cvm ON (cvm.ctype = x.key AND code = x.value)
+                        )foo(y)
+                    )
+                )
+          FROM  metabib.record_attr mra
+          WHERE mra.id = $1;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.bre ( 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$
+DECLARE
+    me      biblio.record_entry%ROWTYPE;
+    layout  unapi.bre_output_layout%ROWTYPE;
+    xfrm    config.xml_transform%ROWTYPE;
+    ouid    INT;
+    tmp_xml TEXT;
+    top_el  TEXT;
+    output  XML;
+    hxml    XML;
+    axml    XML;
+BEGIN
+
+    SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+
+    IF ouid IS NULL THEN
+        RETURN NULL::XML;
+    END IF;
+
+    IF format = 'holdings_xml' THEN -- the special case
+        output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns);
+        RETURN output;
+    END IF;
+
+    SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+    IF layout.name IS NULL THEN
+        RETURN NULL::XML;
+    END IF;
+
+    SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
+
+    SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
+
+    -- grab SVF if we need them
+    IF ('mra' = ANY (includes)) THEN
+        axml := unapi.mra(obj_id,NULL,NULL,NULL,NULL);
+    ELSE
+        axml := NULL::XML;
+    END IF;
+
+    -- grab hodlings if we need them
+    IF ('holdings_xml' = ANY (includes)) THEN
+        hxml := unapi.holdings_xml(obj_id, ouid, org, depth, evergreen.array_remove_item_by_value(includes,'holdings_xml'), slimit, soffset, include_xmlns);
+    ELSE
+        hxml := NULL::XML;
+    END IF;
+
+
+    -- generate our item node
+
+
+    IF format = 'marcxml' THEN
+        tmp_xml := me.marc;
+        IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
+           tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
+        END IF;
+    ELSE
+        tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
+    END IF;
+
+    top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
+
+    IF axml IS NOT NULL THEN
+        tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
+    END IF;
+
+    IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
+        tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
+    END IF;
+
+    IF ('bre.unapi' = ANY (includes)) THEN
+        output := REGEXP_REPLACE(
+            tmp_xml,
+            '</' || top_el || '>(.*?)',
+            XMLELEMENT(
+                name abbr,
+                XMLATTRIBUTES(
+                    'http://www.w3.org/1999/xhtml' AS xmlns,
+                    'unapi-id' AS class,
+                    'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
+                )
+            )::TEXT || '</' || top_el || E'>\\1'
+        );
+    ELSE
+        output := tmp_xml;
+    END IF;
+
+    RETURN output;
+END;
+$F$ LANGUAGE PLPGSQL;
+
+
+-- 0514
+CREATE OR REPLACE FUNCTION unapi.bre ( 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$
+DECLARE
+    me      biblio.record_entry%ROWTYPE;
+    layout  unapi.bre_output_layout%ROWTYPE;
+    xfrm    config.xml_transform%ROWTYPE;
+    ouid    INT;
+    tmp_xml TEXT;
+    top_el  TEXT;
+    output  XML;
+    hxml    XML;
+    axml    XML;
+BEGIN
+
+    SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+
+    IF ouid IS NULL THEN
+        RETURN NULL::XML;
+    END IF;
+
+    IF format = 'holdings_xml' THEN -- the special case
+        output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns);
+        RETURN output;
+    END IF;
+
+    SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+    IF layout.name IS NULL THEN
+        RETURN NULL::XML;
+    END IF;
+
+    SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
+
+    SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
+
+    -- grab SVF if we need them
+    IF ('mra' = ANY (includes)) THEN
+        axml := unapi.mra(obj_id,NULL,NULL,NULL,NULL);
+    ELSE
+        axml := NULL::XML;
+    END IF;
+
+    -- grab hodlings if we need them
+    IF ('holdings_xml' = ANY (includes)) THEN
+        hxml := unapi.holdings_xml(obj_id, ouid, org, depth, evergreen.array_remove_item_by_value(includes,'holdings_xml'), slimit, soffset, include_xmlns);
+    ELSE
+        hxml := NULL::XML;
+    END IF;
+
+
+    -- generate our item node
+
+
+    IF format = 'marcxml' THEN
+        tmp_xml := me.marc;
+        IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
+           tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
+        END IF;
+    ELSE
+        tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
+    END IF;
+
+    top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
+
+    IF axml IS NOT NULL THEN
+        tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
+    END IF;
+
+    IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
+        tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
+    END IF;
+
+    IF ('bre.unapi' = ANY (includes)) THEN
+        output := REGEXP_REPLACE(
+            tmp_xml,
+            '</' || top_el || '>(.*?)',
+            XMLELEMENT(
+                name abbr,
+                XMLATTRIBUTES(
+                    'http://www.w3.org/1999/xhtml' AS xmlns,
+                    'unapi-id' AS class,
+                    'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
+                )
+            )::TEXT || '</' || top_el || E'>\\1'
+        );
+    ELSE
+        output := tmp_xml;
+    END IF;
+
+    output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML;
+    RETURN output;
+END;
+$F$ LANGUAGE PLPGSQL;
+
+
+
+-- 0516
+CREATE OR REPLACE FUNCTION public.extract_acq_marc_field ( BIGINT, TEXT, TEXT) RETURNS TEXT AS $$    
+    SELECT extract_marc_field('acq.lineitem', $1, $2, $3);
+$$ LANGUAGE SQL;
+
+-- 0518
+CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
+DECLARE
+    rtype       TEXT;
+    ff_pos      RECORD;
+    tag_data    RECORD;
+    val         TEXT;
+BEGIN
+    rtype := (vandelay.marc21_record_type( marc )).code;
+    FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
+        IF ff_pos.tag = 'ldr' THEN
+            val := oils_xpath_string('//*[local-name()="leader"]', marc);
+            IF val IS NOT NULL THEN
+                val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
+                RETURN val;
+            END IF;
+        ELSE 
+            FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
+                val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
+                RETURN val;
+            END LOOP;
+        END IF;
+        val := REPEAT( ff_pos.default_val, ff_pos.length );
+        RETURN val;
+    END LOOP;
+
+    RETURN NULL;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+
+-- 0519
+CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT ) RETURNS SETOF biblio.record_ff_map AS $func$
+DECLARE
+    tag_data    TEXT;
+    rtype       TEXT;
+    ff_pos      RECORD;
+    output      biblio.record_ff_map%ROWTYPE;
+BEGIN
+    rtype := (vandelay.marc21_record_type( marc )).code;
+
+    FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE rec_type = rtype ORDER BY tag DESC LOOP
+        output.ff_name  := ff_pos.fixed_field;
+        output.ff_value := NULL;
+
+        IF ff_pos.tag = 'ldr' THEN
+            output.ff_value := oils_xpath_string('//*[local-name()="leader"]', marc);
+            IF output.ff_value IS NOT NULL THEN
+                output.ff_value := SUBSTRING( output.ff_value, ff_pos.start_pos + 1, ff_pos.length );
+                RETURN NEXT output;
+                output.ff_value := NULL;
+            END IF;
+        ELSE
+            FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
+                output.ff_value := SUBSTRING( tag_data, ff_pos.start_pos + 1, ff_pos.length );
+                IF output.ff_value IS NULL THEN output.ff_value := REPEAT( ff_pos.default_val, ff_pos.length ); END IF;
+                RETURN NEXT output;
+                output.ff_value := NULL;
+            END LOOP;
+        END IF;
+    
+    END LOOP;
+
+    RETURN;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+
+-- 0521
+CREATE OR REPLACE FUNCTION biblio.extract_located_uris( bib_id BIGINT, marcxml TEXT, editor_id INT ) RETURNS VOID AS $func$
+DECLARE
+    uris            TEXT[];
+    uri_xml         TEXT;
+    uri_label       TEXT;
+    uri_href        TEXT;
+    uri_use         TEXT;
+    uri_owner_list  TEXT[];
+    uri_owner       TEXT;
+    uri_owner_id    INT;
+    uri_id          INT;
+    uri_cn_id       INT;
+    uri_map_id      INT;
+BEGIN
+
+    -- Clear any URI mappings and call numbers for this bib.
+    -- This leads to acn / auricnm inflation, but also enables
+    -- old acn/auricnm's to go away and for bibs to be deleted.
+    FOR uri_cn_id IN SELECT id FROM asset.call_number WHERE record = bib_id AND label = '##URI##' AND NOT deleted LOOP
+        DELETE FROM asset.uri_call_number_map WHERE call_number = uri_cn_id;
+        DELETE FROM asset.call_number WHERE id = uri_cn_id;
+    END LOOP;
+
+    uris := oils_xpath('//*[@tag="856" and (@ind1="4" or @ind1="1") and (@ind2="0" or @ind2="1")]',marcxml);
+    IF ARRAY_UPPER(uris,1) > 0 THEN
+        FOR i IN 1 .. ARRAY_UPPER(uris, 1) LOOP
+            -- First we pull info out of the 856
+            uri_xml     := uris[i];
+
+            uri_href    := (oils_xpath('//*[@code="u"]/text()',uri_xml))[1];
+            uri_label   := (oils_xpath('//*[@code="y"]/text()|//*[@code="3"]/text()|//*[@code="u"]/text()',uri_xml))[1];
+            uri_use     := (oils_xpath('//*[@code="z"]/text()|//*[@code="2"]/text()|//*[@code="n"]/text()',uri_xml))[1];
+            CONTINUE WHEN uri_href IS NULL OR uri_label IS NULL;
+
+            -- Get the distinct list of libraries wanting to use 
+            SELECT  ARRAY_ACCUM(
+                        DISTINCT REGEXP_REPLACE(
+                            x,
+                            $re$^.*?\((\w+)\).*$$re$,
+                            E'\\1'
+                        )
+                    ) INTO uri_owner_list
+              FROM  UNNEST(
+                        oils_xpath(
+                            '//*[@code="9"]/text()|//*[@code="w"]/text()|//*[@code="n"]/text()',
+                            uri_xml
+                        )
+                    )x;
+
+            IF ARRAY_UPPER(uri_owner_list,1) > 0 THEN
+
+                -- look for a matching uri
+                SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
+                IF NOT FOUND THEN -- create one
+                    INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
+                    IF uri_use IS NULL THEN
+                        SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active;
+                    ELSE
+                        SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
+                    END IF;
+                END IF;
+
+                FOR j IN 1 .. ARRAY_UPPER(uri_owner_list, 1) LOOP
+                    uri_owner := uri_owner_list[j];
+
+                    SELECT id INTO uri_owner_id FROM actor.org_unit WHERE shortname = uri_owner;
+                    CONTINUE WHEN NOT FOUND;
+
+                    -- we need a call number to link through
+                    SELECT id INTO uri_cn_id FROM asset.call_number WHERE owning_lib = uri_owner_id AND record = bib_id AND label = '##URI##' AND NOT deleted;
+                    IF NOT FOUND THEN
+                        INSERT INTO asset.call_number (owning_lib, record, create_date, edit_date, creator, editor, label)
+                            VALUES (uri_owner_id, bib_id, 'now', 'now', editor_id, editor_id, '##URI##');
+                        SELECT id INTO uri_cn_id FROM asset.call_number WHERE owning_lib = uri_owner_id AND record = bib_id AND label = '##URI##' AND NOT deleted;
+                    END IF;
+
+                    -- now, link them if they're not already
+                    SELECT id INTO uri_map_id FROM asset.uri_call_number_map WHERE call_number = uri_cn_id AND uri = uri_id;
+                    IF NOT FOUND THEN
+                        INSERT INTO asset.uri_call_number_map (call_number, uri) VALUES (uri_cn_id, uri_id);
+                    END IF;
+
+                END LOOP;
+
+            END IF;
+
+        END LOOP;
+    END IF;
+
+    RETURN;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+
+-- 0522
+UPDATE config.org_unit_setting_type SET datatype = 'string' WHERE name = 'ui.general.button_bar';
+
+INSERT INTO config.org_unit_setting_type ( name, label, description, datatype) VALUES ('ui.general.hotkeyset', 'GUI: Default Hotkeyset', 'Default Hotkeyset for clients (filename without the .keyset).  Examples: Default, Minimal, and None', 'string');
+
+UPDATE actor.org_unit_setting SET value='"circ"' WHERE name = 'ui.general.button_bar' AND value='true';
+
+UPDATE actor.org_unit_setting SET value='"none"' WHERE name = 'ui.general.button_bar' AND value='false';
+
+
+-- 0523
+INSERT into config.org_unit_setting_type
+( name, label, description, datatype, fm_class ) VALUES
+( 'cat.default_copy_status_fast',
+  oils_i18n_gettext( 'cat.default_copy_status_fast', 'Cataloging: Default copy status (fast add)', 'coust', 'label'),
+  oils_i18n_gettext( 'cat.default_copy_status_fast', 'Default status when a copy is created using the "Fast Add" interface.', 'coust', 'description'),
+  'link', 'ccs'
+);
+
+INSERT into config.org_unit_setting_type
+( name, label, description, datatype, fm_class ) VALUES
+( 'cat.default_copy_status_normal',
+  oils_i18n_gettext( 'cat.default_copy_status_normal', 'Cataloging: Default copy status (normal)', 'coust', 'label'),
+  oils_i18n_gettext( 'cat.default_copy_status_normal', 'Default status when a copy is created using the normal volume/copy creator interface.', 'coust', 'description'),
+  'link', 'ccs'
+);
+
+-- 0524
+INSERT into config.org_unit_setting_type
+( name, label, description, datatype ) VALUES
+( 'ui.unified_volume_copy_editor',
+  oils_i18n_gettext( 'ui.unified_volume_copy_editor', 'GUI: Unified Volume/Item Creator/Editor', 'coust', 'label'),
+  oils_i18n_gettext( 'ui.unified_volume_copy_editor', 'If true combines the Volume/Copy Creator and Item Attribute Editor in some instances.', 'coust', 'description'),
+  'bool'
+);
+
+-- 0525
+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  m.value INTO attr_value
+                      FROM  biblio.marc21_physical_characteristics(NEW.id) v
+                            JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
+                      WHERE v.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;
+
+ALTER TABLE config.circ_matrix_weights ADD COLUMN marc_bib_level NUMERIC(6,2) NOT NULL DEFAULT 0.0;
+
+UPDATE config.circ_matrix_weights
+SET marc_bib_level = marc_vr_format;
+
+ALTER TABLE config.hold_matrix_weights ADD COLUMN marc_bib_level NUMERIC(6, 2) NOT NULL DEFAULT 0.0;
+
+UPDATE config.hold_matrix_weights
+SET marc_bib_level = marc_vr_format;
+
+ALTER TABLE config.circ_matrix_weights ALTER COLUMN marc_bib_level DROP DEFAULT;
+
+ALTER TABLE config.hold_matrix_weights ALTER COLUMN marc_bib_level DROP DEFAULT;
+
+ALTER TABLE config.circ_matrix_matchpoint ADD COLUMN marc_bib_level text;
+
+ALTER TABLE config.hold_matrix_matchpoint ADD COLUMN marc_bib_level text;
+
+CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, item_object asset.copy, user_object actor.usr, renewal BOOL ) RETURNS action.found_circ_matrix_matchpoint AS $func$
+DECLARE
+    cn_object       asset.call_number%ROWTYPE;
+    rec_descriptor  metabib.rec_descriptor%ROWTYPE;
+    cur_matchpoint  config.circ_matrix_matchpoint%ROWTYPE;
+    matchpoint      config.circ_matrix_matchpoint%ROWTYPE;
+    weights         config.circ_matrix_weights%ROWTYPE;
+    user_age        INTERVAL;
+    denominator     NUMERIC(6,2);
+    row_list        INT[];
+    result          action.found_circ_matrix_matchpoint;
+BEGIN
+    -- Assume failure
+    result.success = false;
+
+    -- Fetch useful data
+    SELECT INTO cn_object       * FROM asset.call_number        WHERE id = item_object.call_number;
+    SELECT INTO rec_descriptor  * FROM metabib.rec_descriptor   WHERE record = cn_object.record;
+
+    -- Pre-generate this so we only calc it once
+    IF user_object.dob IS NOT NULL THEN
+        SELECT INTO user_age age(user_object.dob);
+    END IF;
+
+    -- Grab the closest set circ weight setting.
+    SELECT INTO weights cw.*
+      FROM config.weight_assoc wa
+           JOIN config.circ_matrix_weights cw ON (cw.id = wa.circ_weights)
+           JOIN actor.org_unit_ancestors_distance( context_ou ) d ON (wa.org_unit = d.id)
+      WHERE active
+      ORDER BY d.distance
+      LIMIT 1;
+
+    -- No weights? Bad admin! Defaults to handle that anyway.
+    IF weights.id IS NULL THEN
+        weights.grp                 := 11.0;
+        weights.org_unit            := 10.0;
+        weights.circ_modifier       := 5.0;
+        weights.marc_type           := 4.0;
+        weights.marc_form           := 3.0;
+        weights.marc_bib_level      := 2.0;
+        weights.marc_vr_format      := 2.0;
+        weights.copy_circ_lib       := 8.0;
+        weights.copy_owning_lib     := 8.0;
+        weights.user_home_ou        := 8.0;
+        weights.ref_flag            := 1.0;
+        weights.juvenile_flag       := 6.0;
+        weights.is_renewal          := 7.0;
+        weights.usr_age_lower_bound := 0.0;
+        weights.usr_age_upper_bound := 0.0;
+    END IF;
+
+    -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
+    -- If you break your org tree with funky parenting this may be wrong
+    -- Note: This CTE is duplicated in the find_hold_matrix_matchpoint function, and it may be a good idea to split it off to a function
+    -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
+    WITH all_distance(distance) AS (
+            SELECT depth AS distance FROM actor.org_unit_type
+        UNION
+                   SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
+       )
+    SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
+
+    -- Loop over all the potential matchpoints
+    FOR cur_matchpoint IN
+        SELECT m.*
+          FROM  config.circ_matrix_matchpoint m
+                /*LEFT*/ JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.grp = upgad.id
+                /*LEFT*/ JOIN actor.org_unit_ancestors_distance( context_ou ) ctoua ON m.org_unit = ctoua.id
+                LEFT JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) cnoua ON m.copy_owning_lib = cnoua.id
+                LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.copy_circ_lib = iooua.id
+                LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
+          WHERE m.active
+                -- Permission Groups
+             -- AND (m.grp                      IS NULL OR upgad.id IS NOT NULL) -- Optional Permission Group?
+                -- Org Units
+             -- AND (m.org_unit                 IS NULL OR ctoua.id IS NOT NULL) -- Optional Org Unit?
+                AND (m.copy_owning_lib          IS NULL OR cnoua.id IS NOT NULL)
+                AND (m.copy_circ_lib            IS NULL OR iooua.id IS NOT NULL)
+                AND (m.user_home_ou             IS NULL OR uhoua.id IS NOT NULL)
+                -- Circ Type
+                AND (m.is_renewal               IS NULL OR m.is_renewal = renewal)
+                -- Static User Checks
+                AND (m.juvenile_flag            IS NULL OR m.juvenile_flag = user_object.juvenile)
+                AND (m.usr_age_lower_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_lower_bound < user_age))
+                AND (m.usr_age_upper_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_upper_bound > user_age))
+                -- Static Item Checks
+                AND (m.circ_modifier            IS NULL OR m.circ_modifier = item_object.circ_modifier)
+                AND (m.marc_type                IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
+                AND (m.marc_form                IS NULL OR m.marc_form = rec_descriptor.item_form)
+                AND (m.marc_bib_level           IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
+                AND (m.marc_vr_format           IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
+                AND (m.ref_flag                 IS NULL OR m.ref_flag = item_object.ref)
+          ORDER BY
+                -- Permission Groups
+                CASE WHEN upgad.distance        IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0.0 END +
+                -- Org Units
+                CASE WHEN ctoua.distance        IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0.0 END +
+                CASE WHEN cnoua.distance        IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0.0 END +
+                CASE WHEN iooua.distance        IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0.0 END +
+                CASE WHEN uhoua.distance        IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
+                -- Circ Type                    -- Note: 4^x is equiv to 2^(2*x)
+                CASE WHEN m.is_renewal          IS NOT NULL THEN 4^weights.is_renewal ELSE 0.0 END +
+                -- Static User Checks
+                CASE WHEN m.juvenile_flag       IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
+                CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0.0 END +
+                CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0.0 END +
+                -- Static Item Checks
+                CASE WHEN m.circ_modifier       IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
+                CASE WHEN m.marc_type           IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
+                CASE WHEN m.marc_form           IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
+                CASE WHEN m.marc_vr_format      IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
+                CASE WHEN m.ref_flag            IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END DESC,
+                -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
+                -- This prevents "we changed the table order by updating a rule, and we started getting different results"
+                m.id LOOP
+
+        -- Record the full matching row list
+        row_list := row_list || cur_matchpoint.id;
+
+        -- No matchpoint yet?
+        IF matchpoint.id IS NULL THEN
+            -- Take the entire matchpoint as a starting point
+            matchpoint := cur_matchpoint;
+            CONTINUE; -- No need to look at this row any more.
+        END IF;
+
+        -- Incomplete matchpoint?
+        IF matchpoint.circulate IS NULL THEN
+            matchpoint.circulate := cur_matchpoint.circulate;
+        END IF;
+        IF matchpoint.duration_rule IS NULL THEN
+            matchpoint.duration_rule := cur_matchpoint.duration_rule;
+        END IF;
+        IF matchpoint.recurring_fine_rule IS NULL THEN
+            matchpoint.recurring_fine_rule := cur_matchpoint.recurring_fine_rule;
+        END IF;
+        IF matchpoint.max_fine_rule IS NULL THEN
+            matchpoint.max_fine_rule := cur_matchpoint.max_fine_rule;
+        END IF;
+        IF matchpoint.hard_due_date IS NULL THEN
+            matchpoint.hard_due_date := cur_matchpoint.hard_due_date;
+        END IF;
+        IF matchpoint.total_copy_hold_ratio IS NULL THEN
+            matchpoint.total_copy_hold_ratio := cur_matchpoint.total_copy_hold_ratio;
+        END IF;
+        IF matchpoint.available_copy_hold_ratio IS NULL THEN
+            matchpoint.available_copy_hold_ratio := cur_matchpoint.available_copy_hold_ratio;
+        END IF;
+        IF matchpoint.renewals IS NULL THEN
+            matchpoint.renewals := cur_matchpoint.renewals;
+        END IF;
+        IF matchpoint.grace_period IS NULL THEN
+            matchpoint.grace_period := cur_matchpoint.grace_period;
+        END IF;
+    END LOOP;
+
+    -- Check required fields
+    IF matchpoint.circulate             IS NOT NULL AND
+       matchpoint.duration_rule         IS NOT NULL AND
+       matchpoint.recurring_fine_rule   IS NOT NULL AND
+       matchpoint.max_fine_rule         IS NOT NULL THEN
+        -- All there? We have a completed match.
+        result.success := true;
+    END IF;
+
+    -- Include the assembled matchpoint, even if it isn't complete
+    result.matchpoint := matchpoint;
+
+    -- Include (for debugging) the full list of matching rows
+    result.buildrows := row_list;
+
+    -- Hand the result back to caller
+    RETURN result;
+END;
+$func$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
+  RETURNS integer AS
+$func$
+DECLARE
+    requestor_object    actor.usr%ROWTYPE;
+    user_object         actor.usr%ROWTYPE;
+    item_object         asset.copy%ROWTYPE;
+    item_cn_object      asset.call_number%ROWTYPE;
+    rec_descriptor      metabib.rec_descriptor%ROWTYPE;
+    matchpoint          config.hold_matrix_matchpoint%ROWTYPE;
+    weights             config.hold_matrix_weights%ROWTYPE;
+    denominator         NUMERIC(6,2);
+BEGIN
+    SELECT INTO user_object         * FROM actor.usr                WHERE id = match_user;
+    SELECT INTO requestor_object    * FROM actor.usr                WHERE id = match_requestor;
+    SELECT INTO item_object         * FROM asset.copy               WHERE id = match_item;
+    SELECT INTO item_cn_object      * FROM asset.call_number        WHERE id = item_object.call_number;
+    SELECT INTO rec_descriptor      * FROM metabib.rec_descriptor   WHERE record = item_cn_object.record;
+
+    -- The item's owner should probably be the one determining if the item is holdable
+    -- How to decide that is debatable. Decided to default to the circ library (where the item lives)
+    -- This flag will allow for setting it to the owning library (where the call number "lives")
+    PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled;
+
+    -- Grab the closest set circ weight setting.
+    IF NOT FOUND THEN
+        -- Default to circ library
+        SELECT INTO weights hw.*
+          FROM config.weight_assoc wa
+               JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
+               JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id)
+          WHERE active
+          ORDER BY d.distance
+          LIMIT 1;
+    ELSE
+        -- Flag is set, use owning library
+        SELECT INTO weights hw.*
+          FROM config.weight_assoc wa
+               JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
+               JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) d ON (wa.org_unit = d.id)
+          WHERE active
+          ORDER BY d.distance
+          LIMIT 1;
+    END IF;
+
+    -- No weights? Bad admin! Defaults to handle that anyway.
+    IF weights.id IS NULL THEN
+        weights.user_home_ou    := 5.0;
+        weights.request_ou      := 5.0;
+        weights.pickup_ou       := 5.0;
+        weights.item_owning_ou  := 5.0;
+        weights.item_circ_ou    := 5.0;
+        weights.usr_grp         := 7.0;
+        weights.requestor_grp   := 8.0;
+        weights.circ_modifier   := 4.0;
+        weights.marc_type       := 3.0;
+        weights.marc_form       := 2.0;
+        weights.marc_bib_level  := 1.0;
+        weights.marc_vr_format  := 1.0;
+        weights.juvenile_flag   := 4.0;
+        weights.ref_flag        := 0.0;
+    END IF;
+
+    -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
+    -- If you break your org tree with funky parenting this may be wrong
+    -- Note: This CTE is duplicated in the find_circ_matrix_matchpoint function, and it may be a good idea to split it off to a function
+    -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
+    WITH all_distance(distance) AS (
+            SELECT depth AS distance FROM actor.org_unit_type
+        UNION
+            SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
+       )
+    SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
+
+    -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids.
+    -- This may be better implemented as part of the upgrade script?
+    -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set
+    -- Then remove this flag, of course.
+    PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled;
+
+    IF FOUND THEN
+        -- Note: This, to me, is REALLY hacky. I put it in anyway.
+        -- If you can't tell, this is a single call swap on two variables.
+        SELECT INTO user_object.profile, requestor_object.profile
+                    requestor_object.profile, user_object.profile;
+    END IF;
+
+    -- Select the winning matchpoint into the matchpoint variable for returning
+    SELECT INTO matchpoint m.*
+      FROM  config.hold_matrix_matchpoint m
+            /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id
+            LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id
+            LEFT JOIN actor.org_unit_ancestors_distance( pickup_ou ) puoua ON m.pickup_ou = puoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( request_ou ) rqoua ON m.request_ou = rqoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
+      WHERE m.active
+            -- Permission Groups
+         -- AND (m.requestor_grp        IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group?
+            AND (m.usr_grp              IS NULL OR upgad.id IS NOT NULL)
+            -- Org Units
+            AND (m.pickup_ou            IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.request_ou           IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.item_owning_ou       IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.item_circ_ou         IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.user_home_ou         IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match)))
+            -- Static User Checks
+            AND (m.juvenile_flag        IS NULL OR m.juvenile_flag = user_object.juvenile)
+            -- Static Item Checks
+            AND (m.circ_modifier        IS NULL OR m.circ_modifier = item_object.circ_modifier)
+            AND (m.marc_type            IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
+            AND (m.marc_form            IS NULL OR m.marc_form = rec_descriptor.item_form)
+            AND (m.marc_bib_level       IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
+            AND (m.marc_vr_format       IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
+            AND (m.ref_flag             IS NULL OR m.ref_flag = item_object.ref)
+      ORDER BY
+            -- Permission Groups
+            CASE WHEN rpgad.distance    IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0.0 END +
+            CASE WHEN upgad.distance    IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0.0 END +
+            -- Org Units
+            CASE WHEN puoua.distance    IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0.0 END +
+            CASE WHEN rqoua.distance    IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0.0 END +
+            CASE WHEN cnoua.distance    IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0.0 END +
+            CASE WHEN iooua.distance    IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0.0 END +
+            CASE WHEN uhoua.distance    IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
+            -- Static User Checks       -- Note: 4^x is equiv to 2^(2*x)
+            CASE WHEN m.juvenile_flag   IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
+            -- Static Item Checks
+            CASE WHEN m.circ_modifier   IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
+            CASE WHEN m.marc_type       IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
+            CASE WHEN m.marc_form       IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
+            CASE WHEN m.marc_vr_format  IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
+            CASE WHEN m.ref_flag        IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END DESC,
+            -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
+            -- This prevents "we changed the table order by updating a rule, and we started getting different results"
+            m.id;
+
+    -- Return just the ID for now
+    RETURN matchpoint.id;
+END;
+$func$ LANGUAGE 'plpgsql';
+
+-- 0528
+CREATE OR REPLACE FUNCTION maintain_control_numbers() RETURNS TRIGGER AS $func$
+use strict;
+use MARC::Record;
+use MARC::File::XML (BinaryEncoding => 'UTF-8');
+use MARC::Charset;
+use Encode;
+use Unicode::Normalize;
+
+MARC::Charset->assume_unicode(1);
+
+my $record = MARC::Record->new_from_xml($_TD->{new}{marc});
+my $schema = $_TD->{table_schema};
+my $rec_id = $_TD->{new}{id};
+
+# Short-circuit if maintaining control numbers per MARC21 spec is not enabled
+my $enable = spi_exec_query("SELECT enabled FROM config.global_flag WHERE name = 'cat.maintain_control_numbers'");
+if (!($enable->{processed}) or $enable->{rows}[0]->{enabled} eq 'f') {
+    return;
+}
+
+# Get the control number identifier from an OU setting based on $_TD->{new}{owner}
+my $ou_cni = 'EVRGRN';
+
+my $owner;
+if ($schema eq 'serial') {
+    $owner = $_TD->{new}{owning_lib};
+} else {
+    # are.owner and bre.owner can be null, so fall back to the consortial setting
+    $owner = $_TD->{new}{owner} || 1;
+}
+
+my $ous_rv = spi_exec_query("SELECT value FROM actor.org_unit_ancestor_setting('cat.marc_control_number_identifier', $owner)");
+if ($ous_rv->{processed}) {
+    $ou_cni = $ous_rv->{rows}[0]->{value};
+    $ou_cni =~ s/"//g; # Stupid VIM syntax highlighting"
+} else {
+    # Fall back to the shortname of the OU if there was no OU setting
+    $ous_rv = spi_exec_query("SELECT shortname FROM actor.org_unit WHERE id = $owner");
+    if ($ous_rv->{processed}) {
+        $ou_cni = $ous_rv->{rows}[0]->{shortname};
+    }
+}
+
+my ($create, $munge) = (0, 0);
+
+my @scns = $record->field('035');
+
+foreach my $id_field ('001', '003') {
+    my $spec_value;
+    my @controls = $record->field($id_field);
+
+    if ($id_field eq '001') {
+        $spec_value = $rec_id;
+    } else {
+        $spec_value = $ou_cni;
+    }
+
+    # Create the 001/003 if none exist
+    if (scalar(@controls) == 1) {
+        # Only one field; check to see if we need to munge it
+        unless (grep $_->data() eq $spec_value, @controls) {
+            $munge = 1;
+        }
+    } else {
+        # Delete the other fields, as with more than 1 001/003 we do not know which 003/001 to match
+        foreach my $control (@controls) {
+            unless ($control->data() eq $spec_value) {
+                $record->delete_field($control);
+            }
+        }
+        $record->insert_fields_ordered(MARC::Field->new($id_field, $spec_value));
+        $create = 1;
+    }
+}
+
+# Now, if we need to munge the 001, we will first push the existing 001/003
+# into the 035; but if the record did not have one (and one only) 001 and 003
+# to begin with, skip this process
+if ($munge and not $create) {
+    my $scn = "(" . $record->field('003')->data() . ")" . $record->field('001')->data();
+
+    # Do not create duplicate 035 fields
+    unless (grep $_->subfield('a') eq $scn, @scns) {
+        $record->insert_fields_ordered(MARC::Field->new('035', '', '', 'a' => $scn));
+    }
+}
+
+# Set the 001/003 and update the MARC
+if ($create or $munge) {
+    $record->field('001')->data($rec_id);
+    $record->field('003')->data($ou_cni);
+
+    my $xml = $record->as_xml_record();
+    $xml =~ s/\n//sgo;
+    $xml =~ s/^<\?xml.+\?\s*>//go;
+    $xml =~ s/>\s+</></go;
+    $xml =~ s/\p{Cc}//go;
+
+    # Embed a version of OpenILS::Application::AppUtils->entityize()
+    # to avoid having to set PERL5LIB for PostgreSQL as well
+
+    # If we are going to convert non-ASCII characters to XML entities,
+    # we had better be dealing with a UTF8 string to begin with
+    $xml = decode_utf8($xml);
+
+    $xml = NFC($xml);
+
+    # Convert raw ampersands to entities
+    $xml =~ s/&(?!\S+;)/&amp;/gso;
+
+    # Convert Unicode characters to entities
+    $xml =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
+
+    $xml =~ s/[\x00-\x1f]//go;
+    $_TD->{new}{marc} = $xml;
+
+    return "MODIFY";
+}
+
+return;
+$func$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION authority.generate_overlay_template ( TEXT, BIGINT ) RETURNS TEXT AS $func$
+
+    use MARC::Record;
+    use MARC::File::XML (BinaryEncoding => 'UTF-8');
+    use MARC::Charset;
+
+    MARC::Charset->assume_unicode(1);
+
+    my $xml = shift;
+    my $r = MARC::Record->new_from_xml( $xml );
+
+    return undef unless ($r);
+
+    my $id = shift() || $r->subfield( '901' => 'c' );
+    $id =~ s/^\s*(?:\([^)]+\))?\s*(.+)\s*?$/$1/;
+    return undef unless ($id); # We need an ID!
+
+    my $tmpl = MARC::Record->new();
+    $tmpl->encoding( 'UTF-8' );
+
+    my @rule_fields;
+    for my $field ( $r->field( '1..' ) ) { # Get main entry fields from the authority record
+
+        my $tag = $field->tag;
+        my $i1 = $field->indicator(1);
+        my $i2 = $field->indicator(2);
+        my $sf = join '', map { $_->[0] } $field->subfields;
+        my @data = map { @$_ } $field->subfields;
+
+        my @replace_them;
+
+        # Map the authority field to bib fields it can control.
+        if ($tag >= 100 and $tag <= 111) {       # names
+            @replace_them = map { $tag + $_ } (0, 300, 500, 600, 700);
+        } elsif ($tag eq '130') {                # uniform title
+            @replace_them = qw/130 240 440 730 830/;
+        } elsif ($tag >= 150 and $tag <= 155) {  # subjects
+            @replace_them = ($tag + 500);
+        } elsif ($tag >= 180 and $tag <= 185) {  # floating subdivisions
+            @replace_them = qw/100 400 600 700 800 110 410 610 710 810 111 411 611 711 811 130 240 440 730 830 650 651 655/;
+        } else {
+            next;
+        }
+
+        # Dummy up the bib-side data
+        $tmpl->append_fields(
+            map {
+                MARC::Field->new( $_, $i1, $i2, @data )
+            } @replace_them
+        );
+
+        # Construct some 'replace' rules
+        push @rule_fields, map { $_ . $sf . '[0~\)' .$id . '$]' } @replace_them;
+    }
+
+    # Insert the replace rules into the template
+    $tmpl->append_fields(
+        MARC::Field->new( '905' => ' ' => ' ' => 'r' => join(',', @rule_fields ) )
+    );
+
+    $xml = $tmpl->as_xml_record;
+    $xml =~ s/^<\?.+?\?>$//mo;
+    $xml =~ s/\n//sgo;
+    $xml =~ s/>\s+</></sgo;
+
+    return $xml;
+
+$func$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION vandelay.add_field ( target_xml TEXT, source_xml TEXT, field TEXT, force_add INT ) RETURNS TEXT AS $_$
+
+    use MARC::Record;
+    use MARC::File::XML (BinaryEncoding => 'UTF-8');
+    use MARC::Charset;
+    use strict;
+
+    MARC::Charset->assume_unicode(1);
+
+    my $target_xml = shift;
+    my $source_xml = shift;
+    my $field_spec = shift;
+    my $force_add = shift || 0;
+
+    my $target_r = MARC::Record->new_from_xml( $target_xml );
+    my $source_r = MARC::Record->new_from_xml( $source_xml );
+
+    return $target_xml unless ($target_r && $source_r);
+
+    my @field_list = split(',', $field_spec);
+
+    my %fields;
+    for my $f (@field_list) {
+        $f =~ s/^\s*//; $f =~ s/\s*$//;
+        if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
+            my $field = $1;
+            $field =~ s/\s+//;
+            my $sf = $2;
+            $sf =~ s/\s+//;
+            my $match = $3;
+            $match =~ s/^\s*//; $match =~ s/\s*$//;
+            $fields{$field} = { sf => [ split('', $sf) ] };
+            if ($match) {
+                my ($msf,$mre) = split('~', $match);
+                if (length($msf) > 0 and length($mre) > 0) {
+                    $msf =~ s/^\s*//; $msf =~ s/\s*$//;
+                    $mre =~ s/^\s*//; $mre =~ s/\s*$//;
+                    $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
+                }
+            }
+        }
+    }
+
+    for my $f ( keys %fields) {
+        if ( @{$fields{$f}{sf}} ) {
+            for my $from_field ($source_r->field( $f )) {
+                my @tos = $target_r->field( $f );
+                if (!@tos) {
+                    next if (exists($fields{$f}{match}) and !$force_add);
+                    my @new_fields = map { $_->clone } $source_r->field( $f );
+                    $target_r->insert_fields_ordered( @new_fields );
+                } else {
+                    for my $to_field (@tos) {
+                        if (exists($fields{$f}{match})) {
+                            next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
+                        }
+                        my @new_sf = map { ($_ => $from_field->subfield($_)) } @{$fields{$f}{sf}};
+                        $to_field->add_subfields( @new_sf );
+                    }
+                }
+            }
+        } else {
+            my @new_fields = map { $_->clone } $source_r->field( $f );
+            $target_r->insert_fields_ordered( @new_fields );
+        }
+    }
+
+    $target_xml = $target_r->as_xml_record;
+    $target_xml =~ s/^<\?.+?\?>$//mo;
+    $target_xml =~ s/\n//sgo;
+    $target_xml =~ s/>\s+</></sgo;
+
+    return $target_xml;
+
+$_$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION authority.normalize_heading( TEXT ) RETURNS TEXT AS $func$
+    use strict;
+    use warnings;
+
+    use utf8;
+    use MARC::Record;
+    use MARC::File::XML (BinaryEncoding => 'UTF8');
+    use MARC::Charset;
+    use UUID::Tiny ':std';
+
+    MARC::Charset->assume_unicode(1);
+
+    my $xml = shift() or return undef;
+
+    my $r;
+
+    # Prevent errors in XML parsing from blowing out ungracefully
+    eval {
+        $r = MARC::Record->new_from_xml( $xml );
+        1;
+    } or do {
+       return 'BAD_MARCXML_' . create_uuid_as_string(UUID_MD5, $xml);
+    };
+
+    if (!$r) {
+       return 'BAD_MARCXML_' . create_uuid_as_string(UUID_MD5, $xml);
+    }
+
+    # From http://www.loc.gov/standards/sourcelist/subject.html
+    my $thes_code_map = {
+        a => 'lcsh',
+        b => 'lcshac',
+        c => 'mesh',
+        d => 'nal',
+        k => 'cash',
+        n => 'notapplicable',
+        r => 'aat',
+        s => 'sears',
+        v => 'rvm',
+    };
+
+    # Default to "No attempt to code" if the leader is horribly broken
+    my $fixed_field = $r->field('008');
+    my $thes_char = '|';
+    if ($fixed_field) { 
+        $thes_char = substr($fixed_field->data(), 11, 1) || '|';
+    }
+
+    my $thes_code = 'UNDEFINED';
+
+    if ($thes_char eq 'z') {
+        # Grab the 040 $f per http://www.loc.gov/marc/authority/ad040.html
+        $thes_code = $r->subfield('040', 'f') || 'UNDEFINED';
+    } elsif ($thes_code_map->{$thes_char}) {
+        $thes_code = $thes_code_map->{$thes_char};
+    }
+
+    my $auth_txt = '';
+    my $head = $r->field('1..');
+    if ($head) {
+        # Concatenate all of these subfields together, prefixed by their code
+        # to prevent collisions along the lines of "Fiction, North Carolina"
+        foreach my $sf ($head->subfields()) {
+            $auth_txt .= '‡' . $sf->[0] . ' ' . $sf->[1];
+        }
+    }
+    
+    if ($auth_txt) {
+        my $stmt = spi_prepare('SELECT public.naco_normalize($1) AS norm_text', 'TEXT');
+        my $result = spi_exec_prepared($stmt, $auth_txt);
+        my $norm_txt = $result->{rows}[0]->{norm_text};
+        spi_freeplan($stmt);
+        undef($stmt);
+        return $head->tag() . "_" . $thes_code . " " . $norm_txt;
+    }
+
+    return 'NOHEADING_' . $thes_code . ' ' . create_uuid_as_string(UUID_MD5, $xml);
+$func$ LANGUAGE 'plperlu' IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION vandelay.strip_field ( xml TEXT, field TEXT ) RETURNS TEXT AS $_$
+
+    use MARC::Record;
+    use MARC::File::XML (BinaryEncoding => 'UTF-8');
+    use MARC::Charset;
+    use strict;
+
+    MARC::Charset->assume_unicode(1);
+
+    my $xml = shift;
+    my $r = MARC::Record->new_from_xml( $xml );
+
+    return $xml unless ($r);
+
+    my $field_spec = shift;
+    my @field_list = split(',', $field_spec);
+
+    my %fields;
+    for my $f (@field_list) {
+        $f =~ s/^\s*//; $f =~ s/\s*$//;
+        if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
+            my $field = $1;
+            $field =~ s/\s+//;
+            my $sf = $2;
+            $sf =~ s/\s+//;
+            my $match = $3;
+            $match =~ s/^\s*//; $match =~ s/\s*$//;
+            $fields{$field} = { sf => [ split('', $sf) ] };
+            if ($match) {
+                my ($msf,$mre) = split('~', $match);
+                if (length($msf) > 0 and length($mre) > 0) {
+                    $msf =~ s/^\s*//; $msf =~ s/\s*$//;
+                    $mre =~ s/^\s*//; $mre =~ s/\s*$//;
+                    $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
+                }
+            }
+        }
+    }
+
+    for my $f ( keys %fields) {
+        for my $to_field ($r->field( $f )) {
+            if (exists($fields{$f}{match})) {
+                next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
+            }
+
+            if ( @{$fields{$f}{sf}} ) {
+                $to_field->delete_subfield(code => $fields{$f}{sf});
+            } else {
+                $r->delete_field( $to_field );
+            }
+        }
+    }
+
+    $xml = $r->as_xml_record;
+    $xml =~ s/^<\?.+?\?>$//mo;
+    $xml =~ s/\n//sgo;
+    $xml =~ s/>\s+</></sgo;
+
+    return $xml;
+
+$_$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION biblio.flatten_marc ( TEXT ) RETURNS SETOF metabib.full_rec AS $func$
+
+use MARC::Record;
+use MARC::File::XML (BinaryEncoding => 'UTF-8');
+use MARC::Charset;
+
+MARC::Charset->assume_unicode(1);
+
+my $xml = shift;
+my $r = MARC::Record->new_from_xml( $xml );
+
+return_next( { tag => 'LDR', value => $r->leader } );
+
+for my $f ( $r->fields ) {
+       if ($f->is_control_field) {
+               return_next({ tag => $f->tag, value => $f->data });
+       } else {
+               for my $s ($f->subfields) {
+                       return_next({
+                               tag      => $f->tag,
+                               ind1     => $f->indicator(1),
+                               ind2     => $f->indicator(2),
+                               subfield => $s->[0],
+                               value    => $s->[1]
+                       });
+
+                       if ( $f->tag eq '245' and $s->[0] eq 'a' ) {
+                               my $trim = $f->indicator(2) || 0;
+                               return_next({
+                                       tag      => 'tnf',
+                                       ind1     => $f->indicator(1),
+                                       ind2     => $f->indicator(2),
+                                       subfield => 'a',
+                                       value    => substr( $s->[1], $trim )
+                               });
+                       }
+               }
+       }
+}
+
+return undef;
+
+$func$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION authority.flatten_marc ( TEXT ) RETURNS SETOF authority.full_rec AS $func$
+
+use MARC::Record;
+use MARC::File::XML (BinaryEncoding => 'UTF-8');
+use MARC::Charset;
+
+MARC::Charset->assume_unicode(1);
+
+my $xml = shift;
+my $r = MARC::Record->new_from_xml( $xml );
+
+return_next( { tag => 'LDR', value => $r->leader } );
+
+for my $f ( $r->fields ) {
+    if ($f->is_control_field) {
+        return_next({ tag => $f->tag, value => $f->data });
+    } else {
+        for my $s ($f->subfields) {
+            return_next({
+                tag      => $f->tag,
+                ind1     => $f->indicator(1),
+                ind2     => $f->indicator(2),
+                subfield => $s->[0],
+                value    => $s->[1]
+            });
+
+        }
+    }
+}
+
+return undef;
+
+$func$ LANGUAGE PLPERLU;
+
+-- 0529
+INSERT INTO config.org_unit_setting_type 
+( name, label, description, datatype ) VALUES 
+( 'circ.user_merge.delete_addresses', 
+  'Circ:  Patron Merge Address Delete', 
+  'Delete address(es) of subordinate user(s) in a patron merge', 
+   'bool'
+);
+
+INSERT INTO config.org_unit_setting_type 
+( name, label, description, datatype ) VALUES 
+( 'circ.user_merge.delete_cards', 
+  'Circ: Patron Merge Barcode Delete', 
+  'Delete barcode(s) of subordinate user(s) in a patron merge', 
+  'bool'
+);
+
+INSERT INTO config.org_unit_setting_type 
+( name, label, description, datatype ) VALUES 
+( 'circ.user_merge.deactivate_cards', 
+  'Circ:  Patron Merge Deactivate Card', 
+  'Mark barcode(s) of subordinate user(s) in a patron merge as inactive', 
+  'bool'
+);
+
+-- 0530
+CREATE INDEX actor_usr_day_phone_idx_numeric ON actor.usr USING BTREE 
+    (evergreen.lowercase(REGEXP_REPLACE(day_phone, '[^0-9]', '', 'g')));
+
+CREATE INDEX actor_usr_evening_phone_idx_numeric ON actor.usr USING BTREE 
+    (evergreen.lowercase(REGEXP_REPLACE(evening_phone, '[^0-9]', '', 'g')));
+
+CREATE INDEX actor_usr_other_phone_idx_numeric ON actor.usr USING BTREE 
+    (evergreen.lowercase(REGEXP_REPLACE(other_phone, '[^0-9]', '', 'g')));
+
+-- 0533
+CREATE OR REPLACE FUNCTION action.age_circ_on_delete () RETURNS TRIGGER AS $$
+DECLARE
+found char := 'N';
+BEGIN
+
+    -- If there are any renewals for this circulation, don't archive or delete
+    -- it yet.   We'll do so later, when we archive and delete the renewals.
+
+    SELECT 'Y' INTO found
+    FROM action.circulation
+    WHERE parent_circ = OLD.id
+    LIMIT 1;
+
+    IF found = 'Y' THEN
+        RETURN NULL;  -- don't delete
+       END IF;
+
+    -- Archive a copy of the old row to action.aged_circulation
+
+    INSERT INTO action.aged_circulation
+        (id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
+        copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
+        circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
+        stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
+        max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
+        max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ)
+      SELECT
+        id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
+        copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
+        circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
+        stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
+        max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
+        max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
+        FROM action.all_circulation WHERE id = OLD.id;
+
+    RETURN OLD;
+END;
+$$ LANGUAGE 'plpgsql';
+
+-- do potentially large updates last to save time if upgrader needs
+-- to manually tweak the upgrade script to resolve errors
+
+-- 0505
+UPDATE metabib.facet_entry SET value = evergreen.force_unicode_normal_form(value,'NFC');
+
+UPDATE asset.call_number SET id = id;
+
+-- Update reporter.materialized_simple_record with normalized ISBN values
+-- This might not get all of them, but most ISBNs will have more than one hyphen
+DELETE FROM reporter.materialized_simple_record WHERE id IN (
+    SELECT record FROM metabib.full_rec WHERE tag = '020' AND subfield IN ('a', 'z') AND value LIKE '%-%-%'
+);
+
+INSERT INTO reporter.materialized_simple_record
+    SELECT DISTINCT rossr.* FROM reporter.old_super_simple_record rossr INNER JOIN metabib.full_rec mfr ON mfr.record = rossr.id
+        WHERE mfr.tag = '020' AND mfr.subfield IN ('a', 'z') AND mfr.value LIKE '%-%-%'
+;
+
+COMMIT;
index 011178a..38b7b71 100644 (file)
@@ -1397,8 +1397,25 @@ INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, u
 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES
        (7, oils_i18n_gettext(7, 'Acquisitions Administrator', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.acq_admin');
 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES
-       (10, oils_i18n_gettext(10, 'Local System Administrator', 'pgt', 'name'), 3, 
-       oils_i18n_gettext(10, 'System maintenance, configuration, etc.', 'pgt', 'description'), '3 years', TRUE, 'group_application.user.staff.admin.local_admin');
+       (8, oils_i18n_gettext(8, 'Cataloging Administrator', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.cat_admin');
+INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES
+       (9, oils_i18n_gettext(9, 'Circulation Administrator', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.circ_admin');
+INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES
+       (10, oils_i18n_gettext(10, 'Local Administrator', 'pgt', 'name'), 3, 
+       oils_i18n_gettext(10, 'Can do anything at the Branch level', 'pgt', 'description'), '3 years', TRUE, 'group_application.user.staff.admin.local_admin');
+INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES
+       (11, oils_i18n_gettext(11, 'Serials', 'pgt', 'name'), 3, 
+       oils_i18n_gettext(11, 'Serials (includes admin features)', 'pgt', 'description'), '3 years', TRUE, 'group_application.user.staff.serials');
+INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES
+       (12, oils_i18n_gettext(12, 'System Administrator', 'pgt', 'name'), 3, 
+       oils_i18n_gettext(12, 'Can do anything at the System level', 'pgt', 'description'), '3 years', TRUE, 'group_application.user.staff.admin.system_admin');
+INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES
+       (13, oils_i18n_gettext(13, 'Global Administrator', 'pgt', 'name'), 3, 
+       oils_i18n_gettext(13, 'Can do anything at the Consortium level', 'pgt', 'description'), '3 years', TRUE, 'group_application.user.staff.admin.global_admin');
+INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES
+       (14, oils_i18n_gettext(14, 'Data Review', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.data_review');
+INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES
+       (15, oils_i18n_gettext(15, 'Volunteers', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.volunteers');
 
 SELECT SETVAL('permission.grp_tree_id_seq'::TEXT, (SELECT MAX(id) FROM permission.grp_tree));
 
@@ -1411,170 +1428,749 @@ INSERT INTO permission.grp_penalty_threshold (grp,org_unit,penalty,threshold)
 
 SELECT SETVAL('permission.grp_penalty_threshold_id_seq'::TEXT, (SELECT MAX(id) FROM permission.grp_penalty_threshold));
 
--- XXX Incomplete base permission setup.  A patch would be appreciated.
+
 -- Add basic user permissions to the Users group
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (1, (SELECT id FROM permission.perm_list WHERE code = 'OPAC_LOGIN'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (1, (SELECT id FROM permission.perm_list WHERE code = 'MR_HOLDS'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (1, (SELECT id FROM permission.perm_list WHERE code = 'TITLE_HOLDS'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (1, (SELECT id FROM permission.perm_list WHERE code = 'COPY_CHECKIN'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (1, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_MY_CONTAINER'), 0, false);
 
--- Add basic patron permissions to the Patrons group
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (2, (SELECT id FROM permission.perm_list WHERE code = 'RENEW_CIRC'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (2, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_MY_CONTAINER'), 0, false);
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, FALSE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Users' AND
+               aout.name = 'Consortium' AND
+               perm.code IN (
+                       'COPY_CHECKIN',
+                       'CREATE_MY_CONTAINER',
+                       'MR_HOLDS',
+                       'OPAC_LOGIN',
+                       'RENEW_CIRC',
+                       'TITLE_HOLDS',
+                       'user_request.create');
+
+
+-- Add basic user permissions to the Data Review group
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, FALSE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Data Review' AND
+               aout.name = 'Consortium' AND
+               perm.code IN (
+                       'CREATE_COPY_TRANSIT',
+                       'VIEW_BILLING_TYPE',
+                       'VIEW_CIRCULATIONS',
+                       'VIEW_COPY_NOTES',
+                       'VIEW_HOLD',
+                       'VIEW_ORG_SETTINGS',
+                       'VIEW_TITLE_NOTES',
+                       'VIEW_TRANSACTION',
+                       'VIEW_USER',
+                       'VIEW_USER_FINES_SUMMARY',
+                       'VIEW_USER_TRANSACTIONS',
+                       'VIEW_VOLUME_NOTES',
+                       'VIEW_ZIP_DATA');
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, FALSE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Data Review' AND
+               aout.name = 'System' AND
+               perm.code IN (
+                       'COPY_CHECKOUT',
+                       'COPY_HOLDS',
+                       'CREATE_IN_HOUSE_USE',
+                       'CREATE_TRANSACTION',
+                       'OFFLINE_EXECUTE',
+                       'OFFLINE_VIEW',
+                       'STAFF_LOGIN',
+                       'VOLUME_HOLDS');
+
 
 -- Add basic staff permissions to the Staff group
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'STAFF_LOGIN'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VOLUME_HOLDS'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'COPY_HOLDS'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'REQUEST_HOLDS'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_HOLD'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'RENEW_CIRC'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_USER_FINES_SUMMARY'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_USER_TRANSACTIONS'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_MARC'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_MARC'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'IMPORT_MARC'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_VOLUME'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_VOLUME'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'DELETE_VOLUME'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_COPY'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_COPY'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'RENEW_HOLD_OVERRIDE'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_USER'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_USER'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'DELETE_USER'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_USER'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_TRANSIT'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_PERMISSION'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CHECKIN_BYPASS_HOLD_FULFILL'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_PAYMENT'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'SET_CIRC_LOST'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'SET_CIRC_MISSING'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'SET_CIRC_CLAIMS_RETURNED'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_TRANSACTION'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_TRANSACTION'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_BILL'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_CONTAINER'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_CONTAINER'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_ORG_UNIT'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_CIRCULATIONS'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'DELETE_CONTAINER'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_CONTAINER_ITEM'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_PERM_GROUPS'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_PERMIT_CHECKOUT'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_BATCH_COPY'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_PATRON_STAT_CAT'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_COPY_STAT_CAT'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_PATRON_STAT_CAT_ENTRY'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_COPY_STAT_CAT_ENTRY'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_PATRON_STAT_CAT'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_COPY_STAT_CAT'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_PATRON_STAT_CAT_ENTRY'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_COPY_STAT_CAT_ENTRY'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_NON_CAT_TYPE'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_NON_CAT_TYPE'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_IN_HOUSE_USE'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'COPY_CHECKOUT'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_COPY_LOCATION'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_COPY_LOCATION'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_COPY_TRANSIT'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'COPY_TRANSIT_RECEIVE'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_HOLD_PERMIT'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_COPY_CHECKOUT_HISTORY'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'REMOTE_Z3950_QUERY'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'REGISTER_WORKSTATION'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_COPY_NOTES'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_VOLUME_NOTES'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_TITLE_NOTES'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_COPY_NOTE'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_VOLUME_NOTE'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_CONTAINER'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_HOLD_NOTIFICATION'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_HOLD_NOTIFICATION'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'OFFLINE_UPLOAD'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'OFFLINE_VIEW'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_BILLING_TYPE'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (3, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_ORG_SETTINGS'), 1, false);
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, FALSE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Staff' AND
+               aout.name = 'Consortium' AND
+               perm.code IN (
+                       'CREATE_CONTAINER',
+                       'CREATE_CONTAINER_ITEM',
+                       'CREATE_COPY_TRANSIT',
+                       'CREATE_HOLD_NOTIFICATION',
+                       'CREATE_TRANSACTION',
+                       'CREATE_TRANSIT',
+                       'DELETE_CONTAINER',
+                       'DELETE_CONTAINER_ITEM',
+                       'group_application.user',
+                       'group_application.user.patron',
+                       'REGISTER_WORKSTATION',
+                       'REMOTE_Z3950_QUERY',
+                       'REQUEST_HOLDS',
+                       'STAFF_LOGIN',
+                       'TRANSIT_COPY',
+                       'UPDATE_CONTAINER',
+                       'VIEW_CONTAINER',
+                       'VIEW_COPY_CHECKOUT_HISTORY',
+                       'VIEW_COPY_NOTES',
+                       'VIEW_HOLD',
+                       'VIEW_HOLD_NOTIFICATION',
+                       'VIEW_HOLD_PERMIT',
+                       'VIEW_PERM_GROUPS',
+                       'VIEW_PERMISSION',
+                       'VIEW_TITLE_NOTES',
+                       'VIEW_TRANSACTION',
+                       'VIEW_VOLUME_NOTES');
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, FALSE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Staff' AND
+               aout.name = 'System' AND
+               perm.code IN (
+                       'CREATE_USER',
+                       'UPDATE_USER',
+                       'VIEW_BILLING_TYPE',
+                       'VIEW_CIRCULATIONS',
+                       'VIEW_ORG_SETTINGS',
+                       'VIEW_PERMIT_CHECKOUT',
+                       'VIEW_USER',
+                       'VIEW_USER_FINES_SUMMARY',
+                       'VIEW_USER_TRANSACTIONS');
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, FALSE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Staff' AND
+               aout.name = 'Branch' AND
+               perm.code IN (
+                       'CANCEL_HOLDS',
+                       'COPY_CHECKOUT',
+                       'COPY_HOLDS',
+                       'COPY_TRANSIT_RECEIVE',
+                       'CREATE_BILL',
+                       'CREATE_IN_HOUSE_USE',
+                       'CREATE_PAYMENT',
+                       'RENEW_HOLD_OVERRIDE',
+                       'UPDATE_COPY',
+                       'UPDATE_VOLUME',
+                       'VOLUME_HOLDS');
+
 
 -- Add basic cataloguing permissions to the Catalogers group
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (4, (SELECT id FROM permission.perm_list WHERE code = 'COPY_HOLDS'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (4, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_MARC'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (4, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_MARC'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (4, (SELECT id FROM permission.perm_list WHERE code = 'IMPORT_MARC'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (4, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_VOLUME'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (4, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_VOLUME'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (4, (SELECT id FROM permission.perm_list WHERE code = 'DELETE_VOLUME'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (4, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_COPY'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (4, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_COPY'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (4, (SELECT id FROM permission.perm_list WHERE code = 'DELETE_COPY'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (4, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_BATCH_COPY'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (4, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_MFHD_RECORD'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (4, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_MFHD_RECORD'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (4, (SELECT id FROM permission.perm_list WHERE code = 'DELETE_MFHD_RECORD'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (4, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_RECORD'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (4, (SELECT id FROM permission.perm_list WHERE code = 'MERGE_AUTH_RECORDS'), 1, false);
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, FALSE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Catalogers' AND
+               aout.name = 'Consortium' AND
+               perm.code IN (
+                       'ALLOW_ALT_TCN',
+                       'CREATE_BIB_IMPORT_QUEUE',
+                       'CREATE_IMPORT_ITEM',
+                       'CREATE_MARC',
+                       'CREATE_TITLE_NOTE',
+                       'DELETE_BIB_IMPORT_QUEUE',
+                       'DELETE_IMPORT_ITEM',
+                       'DELETE_RECORD',
+                       'DELETE_TITLE_NOTE',
+                       'IMPORT_ACQ_LINEITEM_BIB_RECORD',
+                       'IMPORT_MARC',
+                       'MERGE_AUTH_RECORDS',
+                       'MERGE_BIB_RECORDS',
+                       'UPDATE_AUTHORITY_IMPORT_QUEUE',
+                       'UPDATE_AUTHORITY_RECORD_NOTE',
+                       'UPDATE_BIB_IMPORT_QUEUE',
+                       'UPDATE_MARC',
+                       'UPDATE_RECORD',
+                       'user_request.view',
+                       'VIEW_AUTHORITY_RECORD_NOTES');
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, FALSE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Catalogers' AND
+               aout.name = 'System' AND
+               perm.code IN (
+                       'CREATE_COPY',
+                       'CREATE_COPY_NOTE',
+                       'CREATE_MFHD_RECORD',
+                       'CREATE_VOLUME',
+                       'CREATE_VOLUME_NOTE',
+                       'DELETE_COPY',
+                       'DELETE_COPY_NOTE',
+                       'DELETE_MFHD_RECORD',
+                       'DELETE_VOLUME',
+                       'DELETE_VOLUME_NOTE',
+                       'MARK_ITEM_AVAILABLE',
+                       'MARK_ITEM_BINDERY',
+                       'MARK_ITEM_CHECKED_OUT',
+                       'MARK_ITEM_ILL',
+                       'MARK_ITEM_IN_PROCESS',
+                       'MARK_ITEM_IN_TRANSIT',
+                       'MARK_ITEM_LOST',
+                       'MARK_ITEM_MISSING',
+                       'MARK_ITEM_ON_HOLDS_SHELF',
+                       'MARK_ITEM_ON_ORDER',
+                       'MARK_ITEM_RESHELVING',
+                       'UPDATE_COPY',
+                       'UPDATE_COPY_NOTE',
+                       'UPDATE_IMPORT_ITEM',
+                       'UPDATE_MFHD_RECORD',
+                       'UPDATE_VOLUME',
+                       'UPDATE_VOLUME_NOTE',
+                       'VIEW_SERIAL_SUBSCRIPTION');
+
+
+-- Add advanced cataloguing permissions to the Cataloging Admin group
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, TRUE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Cataloging Admin' AND
+               aout.name = 'Consortium' AND
+               perm.code IN (
+                       'ADMIN_IMPORT_ITEM_ATTR_DEF',
+                       'ADMIN_MERGE_PROFILE',
+                       'CREATE_AUTHORITY_IMPORT_IMPORT_DEF',
+                       'CREATE_BIB_IMPORT_FIELD_DEF',
+                       'CREATE_BIB_SOURCE',
+                       'CREATE_IMPORT_ITEM_ATTR_DEF',
+                       'CREATE_IMPORT_TRASH_FIELD',
+                       'CREATE_MERGE_PROFILE',
+                       'DELETE_AUTHORITY_IMPORT_IMPORT_FIELD_DEF',
+                       'DELETE_BIB_SOURCE',
+                       'DELETE_IMPORT_ITEM_ATTR_DEF',
+                       'DELETE_IMPORT_TRASH_FIELD',
+                       'DELETE_MERGE_PROFILE',
+                       'UPDATE_AUTHORITY_IMPORT_IMPORT_FIELD_DEF',
+                       'UPDATE_BIB_IMPORT_IMPORT_FIELD_DEF',
+                       'UPDATE_IMPORT_ITEM_ATTR_DEF',
+                       'UPDATE_IMPORT_TRASH_FIELD',
+                       'UPDATE_MERGE_PROFILE');
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, TRUE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Cataloging Admin' AND
+               aout.name = 'System' AND
+               perm.code IN (
+                       'CREATE_COPY_STAT_CAT',
+                       'CREATE_COPY_STAT_CAT_ENTRY',
+                       'CREATE_COPY_STAT_CAT_ENTRY_MAP',
+                       'RUN_REPORTS',
+                       'SHARE_REPORT_FOLDER',
+                       'UPDATE_COPY_LOCATION',
+                       'UPDATE_COPY_STAT_CAT',
+                       'UPDATE_COPY_STAT_CAT_ENTRY',
+                       'VIEW_REPORT_OUTPUT');
+
 
 -- Add basic circulation permissions to the Circulators group
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (5, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_TRANSACTION'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (5, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_BILL'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (5, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_CIRCULATIONS'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (5, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_PERM_GROUPS'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (5, (SELECT id FROM permission.perm_list WHERE code = 'CIRC_OVERRIDE_DUE_DATE'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (5, (SELECT id FROM permission.perm_list WHERE code = 'COPY_IS_REFERENCE.override'), 1, false);
-
--- Add basic sys admin permissions to the Local System Administrator group
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (10, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_USER_GROUP_LINK'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (10, (SELECT id FROM permission.perm_list WHERE code = 'DELETE_PATRON_STAT_CAT'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (10, (SELECT id FROM permission.perm_list WHERE code = 'DELETE_COPY_STAT_CAT'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (10, (SELECT id FROM permission.perm_list WHERE code = 'DELETE_PATRON_STAT_CAT_ENTRY'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (10, (SELECT id FROM permission.perm_list WHERE code = 'DELETE_COPY_STAT_CAT_ENTRY'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (10, (SELECT id FROM permission.perm_list WHERE code = 'DELETE_PATRON_STAT_CAT_ENTRY_MAP'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (10, (SELECT id FROM permission.perm_list WHERE code = 'DELETE_COPY_STAT_CAT_ENTRY_MAP'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (10, (SELECT id FROM permission.perm_list WHERE code = 'DELETE_COPY_LOCATION'), 2, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (10, (SELECT id FROM permission.perm_list WHERE code = 'DELETE_COPY_NOTE'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (10, (SELECT id FROM permission.perm_list WHERE code = 'DELETE_VOLUME_NOTE'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (10, (SELECT id FROM permission.perm_list WHERE code = 'DELETE_TITLE_NOTE'), 0, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (10, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_ORG_SETTING'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (10, (SELECT id FROM permission.perm_list WHERE code = 'OFFLINE_EXECUTE'), 1, true);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (10, (SELECT id FROM permission.perm_list WHERE code = 'CIRC_OVERRIDE_DUE_DATE'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (10, (SELECT id FROM permission.perm_list WHERE code = 'CIRC_PERMIT_OVERRIDE'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (10, (SELECT id FROM permission.perm_list WHERE code = 'RUN_REPORTS'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (10, (SELECT id FROM permission.perm_list WHERE code = 'SHARE_REPORT_FOLDER'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (10, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_REPORT_OUTPUT'), 1, false);
-
--- Add trigger administration permissions to the Local System Administrator group
+
 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
-    SELECT 10, id, 1, false FROM permission.perm_list
-        WHERE code LIKE 'ADMIN_TRIGGER%'
-            OR code LIKE 'CREATE_TRIGGER%'
-            OR code LIKE 'DELETE_TRIGGER%'
-            OR code LIKE 'UPDATE_TRIGGER%'
-;
--- View trigger permissions are required at a consortial level for initial setup
+       SELECT
+               pgt.id, perm.id, aout.depth, FALSE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Circulators' AND
+               aout.name = 'Branch' AND
+               perm.code IN (
+                       'ADMIN_BOOKING_RESERVATION',
+                       'ADMIN_BOOKING_RESOURCE',
+                       'ADMIN_BOOKING_RESOURCE_ATTR',
+                       'ADMIN_BOOKING_RESOURCE_ATTR_MAP',
+                       'ADMIN_BOOKING_RESOURCE_ATTR_VALUE',
+                       'ADMIN_BOOKING_RESOURCE_TYPE',
+                       'ASSIGN_GROUP_PERM',
+                       'MARK_ITEM_AVAILABLE',
+                       'MARK_ITEM_BINDERY',
+                       'MARK_ITEM_CHECKED_OUT',
+                       'MARK_ITEM_ILL',
+                       'MARK_ITEM_IN_PROCESS',
+                       'MARK_ITEM_IN_TRANSIT',
+                       'MARK_ITEM_LOST',
+                       'MARK_ITEM_MISSING',
+                       'MARK_ITEM_ON_HOLDS_SHELF',
+                       'MARK_ITEM_ON_ORDER',
+                       'MARK_ITEM_RESHELVING',
+                       'OFFLINE_UPLOAD',
+                       'OFFLINE_VIEW',
+                       'REMOVE_USER_GROUP_LINK',
+                       'SET_CIRC_CLAIMS_RETURNED',
+                       'SET_CIRC_CLAIMS_RETURNED.override',
+                       'SET_CIRC_LOST',
+                       'SET_CIRC_MISSING',
+                       'UPDATE_BILL_NOTE',
+                       'UPDATE_PATRON_CLAIM_NEVER_CHECKED_OUT_COUNT',
+                       'UPDATE_PATRON_CLAIM_RETURN_COUNT',
+                       'UPDATE_PAYMENT_NOTE',
+                       'UPDATE_PICKUP_LIB FROM_TRANSIT',
+                       'UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF',
+                       'VIEW_GROUP_PENALTY_THRESHOLD',
+                       'VIEW_STANDING_PENALTY',
+                       'VOID_BILLING',
+                       'VOLUME_HOLDS');
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, FALSE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Circulators' AND
+               aout.name = 'System' AND
+               perm.code IN (
+                       'ABORT_REMOTE_TRANSIT',
+                       'ABORT_TRANSIT',
+                       'CAPTURE_RESERVATION',
+                       'CIRC_CLAIMS_RETURNED.override',
+                       'CIRC_EXCEEDS_COPY_RANGE.override',
+                       'CIRC_OVERRIDE_DUE_DATE',
+                       'CIRC_PERMIT_OVERRIDE',
+                       'COPY_ALERT_MESSAGE.override',
+                       'COPY_BAD_STATUS.override',
+                       'COPY_CIRC_NOT_ALLOWED.override',
+                       'COPY_IS_REFERENCE.override',
+                       'COPY_NEEDED_FOR_HOLD.override',
+                       'COPY_NOT_AVAILABLE.override',
+                       'COPY_STATUS_LOST.override',
+                       'COPY_STATUS_MISSING.override',
+                       'CREATE_DUPLICATE_HOLDS',
+                       'CREATE_USER_GROUP_LINK',
+                       'DELETE_TRANSIT',
+                       'HOLD_EXISTS.override',
+                       'HOLD_ITEM_CHECKED_OUT.override',
+                       'ISSUANCE_HOLDS',
+                       'ITEM_AGE_PROTECTED.override',
+                       'ITEM_ON_HOLDS_SHELF.override',
+                       'MAX_RENEWALS_REACHED.override',
+                       'OVERRIDE_HOLD_HAS_LOCAL_COPY',
+                       'PATRON_EXCEEDS_CHECKOUT_COUNT.override',
+                       'PATRON_EXCEEDS_FINES.override',
+                       'PATRON_EXCEEDS_OVERDUE_COUNT.override',
+                       'RETRIEVE_RESERVATION_PULL_LIST',
+                       'UPDATE_HOLD');
+
+
+-- Add advanced circulation permissions to the Circulation Admin group
+
 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
-    SELECT 10, id, 0, false FROM permission.perm_list WHERE code LIKE 'VIEW_TRIGGER%';
+       SELECT
+               pgt.id, perm.id, aout.depth, TRUE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Circulation Admin' AND
+               aout.name = 'Branch' AND
+               perm.code IN (
+                       'DELETE_USER');
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, TRUE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Circulation Admin' AND
+               aout.name = 'Consortium' AND
+               perm.code IN (
+                       'ADMIN_MAX_FINE_RULE',
+                       'CREATE_CIRC_DURATION',
+                       'DELETE_CIRC_DURATION',
+                       'UPDATE_CIRC_DURATION',
+                       'UPDATE_NET_ACCESS_LEVEL',
+                       'VIEW_CIRC_MATRIX_MATCHPOINT',
+                       'VIEW_HOLD_MATRIX_MATCHPOINT');
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, TRUE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Circulation Admin' AND
+               aout.name = 'System' AND
+               perm.code IN (
+                       'ADMIN_BOOKING_RESERVATION',
+                       'ADMIN_BOOKING_RESERVATION_ATTR_MAP',
+                       'ADMIN_BOOKING_RESERVATION_ATTR_VALUE_MAP',
+                       'ADMIN_BOOKING_RESOURCE',
+                       'ADMIN_BOOKING_RESOURCE_ATTR',
+                       'ADMIN_BOOKING_RESOURCE_ATTR_MAP',
+                       'ADMIN_BOOKING_RESOURCE_ATTR_VALUE',
+                       'ADMIN_BOOKING_RESOURCE_TYPE',
+                       'ADMIN_COPY_LOCATION_ORDER',
+                       'ADMIN_HOLD_CANCEL_CAUSE',
+                       'ASSIGN_GROUP_PERM',
+                       'BAR_PATRON',
+                       'COPY_HOLDS',
+                       'COPY_TRANSIT_RECEIVE',
+                       'CREATE_BILL',
+                       'CREATE_BILLING_TYPE',
+                       'CREATE_NON_CAT_TYPE',
+                       'CREATE_PATRON_STAT_CAT',
+                       'CREATE_PATRON_STAT_CAT_ENTRY',
+                       'CREATE_PATRON_STAT_CAT_ENTRY_MAP',
+                       'CREATE_USER_GROUP_LINK',
+                       'DELETE_BILLING_TYPE',
+                       'DELETE_NON_CAT_TYPE',
+                       'DELETE_PATRON_STAT_CAT',
+                       'DELETE_PATRON_STAT_CAT_ENTRY',
+                       'DELETE_PATRON_STAT_CAT_ENTRY_MAP',
+                       'DELETE_TRANSIT',
+                       'group_application.user.staff',
+                       'MANAGE_BAD_DEBT',
+                       'MARK_ITEM_AVAILABLE',
+                       'MARK_ITEM_BINDERY',
+                       'MARK_ITEM_CHECKED_OUT',
+                       'MARK_ITEM_ILL',
+                       'MARK_ITEM_IN_PROCESS',
+                       'MARK_ITEM_IN_TRANSIT',
+                       'MARK_ITEM_LOST',
+                       'MARK_ITEM_MISSING',
+                       'MARK_ITEM_ON_HOLDS_SHELF',
+                       'MARK_ITEM_ON_ORDER',
+                       'MARK_ITEM_RESHELVING',
+                       'MERGE_USERS',
+                       'money.collections_tracker.create',
+                       'money.collections_tracker.delete',
+                       'OFFLINE_EXECUTE',
+                       'OFFLINE_UPLOAD',
+                       'OFFLINE_VIEW',
+                       'REMOVE_USER_GROUP_LINK',
+                       'SET_CIRC_CLAIMS_RETURNED',
+                       'SET_CIRC_CLAIMS_RETURNED.override',
+                       'SET_CIRC_LOST',
+                       'SET_CIRC_MISSING',
+                       'UNBAR_PATRON',
+                       'UPDATE_BILL_NOTE',
+                       'UPDATE_NON_CAT_TYPE',
+                       'UPDATE_PATRON_CLAIM_NEVER_CHECKED_OUT_COUNT',
+                       'UPDATE_PATRON_CLAIM_RETURN_COUNT',
+                       'UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF',
+                       'UPDATE_PICKUP_LIB_FROM_TRANSIT',
+                       'UPDATE_USER',
+                       'VIEW_REPORT_OUTPUT',
+                       'VIEW_STANDING_PENALTY',
+                       'VOID_BILLING',
+                       'VOLUME_HOLDS');
+
+
+-- Add basic sys admin permissions to the Local Administrator group
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, TRUE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Local Administrator' AND
+               aout.name = 'Branch' AND
+               perm.code IN (
+                       'EVERYTHING');
+
+
+-- Add administration permissions to the System Administrator group
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, TRUE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'System Administrator' AND
+               aout.name = 'System' AND
+               perm.code IN (
+                       'EVERYTHING');
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, FALSE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'System Administrator' AND
+               aout.name = 'Consortium' AND
+               perm.code ~ '^VIEW_TRIGGER';
+
+
+-- Add administration permissions to the Global Administrator group
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, TRUE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Global Administrator' AND
+               aout.name = 'Consortium' AND
+               perm.code IN (
+                       'EVERYTHING');
+
 
 -- Add basic acquisitions permissions to the Acquisitions group
+
 SELECT SETVAL('permission.grp_perm_map_id_seq'::TEXT, (SELECT MAX(id) FROM permission.grp_perm_map));
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (6, (SELECT id FROM permission.perm_list WHERE code = 'GENERAL_ACQ'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (6, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_PICKLIST'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (6, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_PICKLIST'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (6, (SELECT id FROM permission.perm_list WHERE code = 'CREATE_PURCHASE_ORDER'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (6, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_PURCHASE_ORDER'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (6, (SELECT id FROM permission.perm_list WHERE code = 'RECEIVE_PURCHASE_ORDER'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (6, (SELECT id FROM permission.perm_list WHERE code = 'VIEW_PROVIDER'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (6, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_COPY'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (6, (SELECT id FROM permission.perm_list WHERE code = 'UPDATE_VOLUME'), 1, false);
-
--- Add acquisitions administration permissions to the Acquisitions group
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (7, (SELECT id FROM permission.perm_list WHERE code = 'ADMIN_PROVIDER'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (7, (SELECT id FROM permission.perm_list WHERE code = 'ADMIN_FUNDING_SOURCE'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (7, (SELECT id FROM permission.perm_list WHERE code = 'ADMIN_ACQ_FUND'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (7, (SELECT id FROM permission.perm_list WHERE code = 'ADMIN_FUND'), 1, false);
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (7, (SELECT id FROM permission.perm_list WHERE code = 'ADMIN_CURRENCY_TYPE'), 1, false);
-
-INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) VALUES (1, (SELECT id FROM permission.perm_list WHERE code = 'HOLD_ITEM_CHECKED_OUT.override'), 0, false);
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, FALSE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Acquisitions' AND
+               aout.name = 'Consortium' AND
+               perm.code IN (
+                       'ALLOW_ALT_TCN',
+                       'CREATE_BIB_IMPORT_QUEUE',
+                       'CREATE_IMPORT_ITEM',
+                       'CREATE_INVOICE',
+                       'CREATE_MARC',
+                       'CREATE_PICKLIST',
+                       'CREATE_PURCHASE_ORDER',
+                       'DELETE_BIB_IMPORT_QUEUE',
+                       'DELETE_IMPORT_ITEM',
+                       'DELETE_RECORD',
+                       'DELETE_VOLUME',
+                       'DELETE_VOLUME_NOTE',
+                       'GENERAL_ACQ',
+                       'IMPORT_ACQ_LINEITEM_BIB_RECORD',
+                       'IMPORT_MARC',
+                       'MANAGE_CLAIM',
+                       'MANAGE_FUND',
+                       'MANAGE_FUNDING_SOURCE',
+                       'MANAGE_PROVIDER',
+                       'MARK_ITEM_AVAILABLE',
+                       'MARK_ITEM_BINDERY',
+                       'MARK_ITEM_CHECKED_OUT',
+                       'MARK_ITEM_ILL',
+                       'MARK_ITEM_IN_PROCESS',
+                       'MARK_ITEM_IN_TRANSIT',
+                       'MARK_ITEM_LOST',
+                       'MARK_ITEM_MISSING',
+                       'MARK_ITEM_ON_HOLDS_SHELF',
+                       'MARK_ITEM_ON_ORDER',
+                       'MARK_ITEM_RESHELVING',
+                       'RECEIVE_PURCHASE_ORDER',
+                       'UPDATE_BATCH_COPY',
+                       'UPDATE_BIB_IMPORT_QUEUE',
+                       'UPDATE_COPY',
+                       'UPDATE_FUND',
+                       'UPDATE_FUND_ALLOCATION',
+                       'UPDATE_FUNDING_SOURCE',
+                       'UPDATE_IMPORT_ITEM',
+                       'UPDATE_MARC',
+                       'UPDATE_RECORD',
+                       'UPDATE_VOLUME',
+                       'user_request.delete',
+                       'user_request.update',
+                       'user_request.view',
+                       'VIEW_ACQ_FUND_ALLOCATION_PERCENT',
+                       'VIEW_ACQ_FUNDING_SOURCE',
+                       'VIEW_FUND',
+                       'VIEW_FUND_ALLOCATION',
+                       'VIEW_FUNDING_SOURCE',
+                       'VIEW_HOLDS',
+                       'VIEW_INVOICE',
+                       'VIEW_ORG_SETTINGS',
+                       'VIEW_PICKLIST',
+                       'VIEW_PROVIDER',
+                       'VIEW_PURCHASE_ORDER',
+                       'VIEW_REPORT_OUTPUT');
+
+
+-- Add acquisitions administration permissions to the Acquisitions Admin group
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, TRUE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Acquisitions Administrator' AND
+               aout.name = 'Consortium' AND
+               perm.code IN (
+                       'ACQ_XFER_MANUAL_DFUND_AMOUNT',
+                       'ADMIN_ACQ_CANCEL_CAUSE',
+                       'ADMIN_ACQ_CLAIM',
+                       'ADMIN_ACQ_CLAIM_EVENT_TYPE',
+                       'ADMIN_ACQ_CLAIM_TYPE',
+                       'ADMIN_ACQ_DISTRIB_FORMULA',
+                       'ADMIN_ACQ_FISCAL_YEAR',
+                       'ADMIN_ACQ_FUND',
+                       'ADMIN_ACQ_FUND_ALLOCATION_PERCENT',
+                       'ADMIN_ACQ_FUND_TAG',
+                       'ADMIN_ACQ_LINE_ITEM_ALERT_TEXT',
+                       'ADMIN_CLAIM_POLICY',
+                       'ADMIN_CURRENCY_TYPE',
+                       'ADMIN_FUND',
+                       'ADMIN_FUNDING_SOURCE',
+                       'ADMIN_INVOICE',
+                       'ADMIN_INVOICE_METHOD',
+                       'ADMIN_INVOICE_PAYMENT_METHOD',
+                       'ADMIN_LINEITEM_MARC_ATTR_DEF',
+                       'ADMIN_PROVIDER',
+                       'ADMIN_USER_REQUEST_TYPE',
+                       'CREATE_ACQ_FUNDING_SOURCE',
+                       'CREATE_FUND',
+                       'CREATE_FUND_ALLOCATION',
+                       'CREATE_FUNDING_SOURCE',
+                       'CREATE_INVOICE_ITEM_TYPE',
+                       'CREATE_INVOICE_METHOD',
+                       'CREATE_PROVIDER',
+                       'DELETE_ACQ_FUNDING_SOURCE',
+                       'DELETE_FUND',
+                       'DELETE_FUND_ALLOCATION',
+                       'DELETE_FUNDING_SOURCE',
+                       'DELETE_INVOICE_ITEM_TYPE',
+                       'DELETE_INVOICE_METHOD',
+                       'DELETE_PROVIDER',
+                       'RUN_REPORTS',
+                       'SHARE_REPORT_FOLDER',
+                       'UPDATE_ACQ_FUNDING_SOURCE',
+                       'UPDATE_INVOICE_ITEM_TYPE',
+                       'UPDATE_INVOICE_METHOD');
+
+
+-- Add serials permissions to the Serials group
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, FALSE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Serials' AND
+               aout.name = 'System' AND
+               perm.code IN (
+                       'ADMIN_ASSET_COPY_TEMPLATE',
+                       'ADMIN_SERIAL_CAPTION_PATTERN',
+                       'ADMIN_SERIAL_DISTRIBUTION',
+                       'ADMIN_SERIAL_STREAM',
+                       'ADMIN_SERIAL_SUBSCRIPTION',
+                       'ISSUANCE_HOLDS',
+                       'RECEIVE_SERIAL');
+
+
+-- Add basic staff permissions to the Volunteers group
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, FALSE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Volunteers' AND
+               aout.name = 'Branch' AND
+               perm.code IN (
+                       'COPY_CHECKOUT',
+                       'CREATE_BILL',
+                       'CREATE_IN_HOUSE_USE',
+                       'CREATE_PAYMENT',
+                       'VIEW_BILLING_TYPE',
+                       'VIEW_CIRCS',
+                       'VIEW_COPY_CHECKOUT',
+                       'VIEW_HOLD',
+                       'VIEW_TITLE_HOLDS',
+                       'VIEW_TRANSACTION',
+                       'VIEW_USER',
+                       'VIEW_USER_FINES_SUMMARY',
+                       'VIEW_USER_TRANSACTIONS');
+
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+       SELECT
+               pgt.id, perm.id, aout.depth, FALSE
+       FROM
+               permission.grp_tree pgt,
+               permission.perm_list perm,
+               actor.org_unit_type aout
+       WHERE
+               pgt.name = 'Volunteers' AND
+               aout.name = 'Consortium' AND
+               perm.code IN (
+                       'CREATE_COPY_TRANSIT',
+                       'CREATE_TRANSACTION',
+                       'CREATE_TRANSIT',
+                       'STAFF_LOGIN',
+                       'TRANSIT_COPY',
+                       'VIEW_ORG_SETTINGS');
+
 
 -- Admin user account
 INSERT INTO actor.usr ( profile, card, usrname, passwd, first_given_name, family_name, dob, master_account, super_user, ident_type, ident_value, home_ou ) VALUES ( 1, 1, md5(random()::text), md5(random()::text), 'Administrator', 'System Account', '1979-01-22', TRUE, TRUE, 1, 'identification', 1 );
@@ -1603,20 +2199,20 @@ INSERT INTO asset.call_number VALUES (-1,1,NOW(),1,NOW(),-1,1,'UNCATALOGED');
 -- circ matrix
 INSERT INTO config.circ_matrix_matchpoint (org_unit,grp,circulate,duration_rule,recurring_fine_rule,max_fine_rule) VALUES (1,1,true,11,1,1);
 
-INSERT INTO config.circ_matrix_weights(name, org_unit, grp, circ_modifier, marc_type, marc_form, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_upper_bound, usr_age_lower_bound) VALUES 
-    ('Default', 10.0, 11.0, 5.0, 4.0, 3.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
-    ('Org_Unit_First', 11.0, 10.0, 5.0, 4.0, 3.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
-    ('Item_Owner_First', 8.0, 8.0, 5.0, 4.0, 3.0, 2.0, 10.0, 11.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
-    ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
+INSERT INTO config.circ_matrix_weights(name, org_unit, grp, circ_modifier, marc_type, marc_form, marc_bib_level, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_upper_bound, usr_age_lower_bound) VALUES 
+    ('Default', 10.0, 11.0, 5.0, 4.0, 3.0, 2.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
+    ('Org_Unit_First', 11.0, 10.0, 5.0, 4.0, 3.0, 2.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
+    ('Item_Owner_First', 8.0, 8.0, 5.0, 4.0, 3.0, 2.0, 2.0, 10.0, 11.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
+    ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
 
 -- hold matrix - 110.hold_matrix.sql:
 INSERT INTO config.hold_matrix_matchpoint (requestor_grp) VALUES (1);
 
-INSERT INTO config.hold_matrix_weights(name, user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_vr_format, juvenile_flag, ref_flag) VALUES
-    ('Default', 5.0, 5.0, 5.0, 5.0, 5.0, 7.0, 8.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
-    ('Item_Owner_First', 5.0, 5.0, 5.0, 8.0, 7.0, 5.0, 5.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
-    ('User_Before_Requestor', 5.0, 5.0, 5.0, 5.0, 5.0, 8.0, 7.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
-    ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
+INSERT INTO config.hold_matrix_weights(name, user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_bib_level, marc_vr_format, juvenile_flag, ref_flag) VALUES
+    ('Default', 5.0, 5.0, 5.0, 5.0, 5.0, 7.0, 8.0, 4.0, 3.0, 2.0, 1.0, 1.0, 4.0, 0.0),
+    ('Item_Owner_First', 5.0, 5.0, 5.0, 8.0, 7.0, 5.0, 5.0, 4.0, 3.0, 2.0, 1.0, 1.0, 4.0, 0.0),
+    ('User_Before_Requestor', 5.0, 5.0, 5.0, 5.0, 5.0, 8.0, 7.0, 4.0, 3.0, 2.0, 1.0, 1.0, 4.0, 0.0),
+    ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
 
 -- dynamic weight associations
 INSERT INTO config.weight_assoc(active, org_unit, circ_weights, hold_weights) VALUES
@@ -7984,3 +8580,40 @@ INSERT into config.org_unit_setting_type
   oils_i18n_gettext( 'cat.default_copy_status_normal', 'Default status when a copy is created using the normal volume/copy creator interface.', 'coust', 'description'),
   'link', 'ccs'
 );
+
+-- 0524.data.toggle_unified_volume_copy_editor.sql
+
+INSERT into config.org_unit_setting_type
+( name, label, description, datatype ) VALUES
+( 'ui.unified_volume_copy_editor',
+  oils_i18n_gettext( 'ui.unified_volume_copy_editor', 'GUI: Unified Volume/Item Creator/Editor', 'coust', 'label'),
+  oils_i18n_gettext( 'ui.unified_volume_copy_editor', 'If true combines the Volume/Copy Creator and Item Attribute Editor in some instances.', 'coust', 'description'),
+  'bool'
+);
+
+-- add patron merge org unit settings
+
+INSERT INTO config.org_unit_setting_type 
+( name, label, description, datatype ) VALUES 
+( 'circ.user_merge.delete_addresses', 
+  'Circ:  Patron Merge Address Delete', 
+  'Delete address(es) of subordinate user(s) in a patron merge', 
+   'bool'
+);
+
+INSERT INTO config.org_unit_setting_type 
+( name, label, description, datatype ) VALUES 
+( 'circ.user_merge.delete_cards', 
+  'Circ: Patron Merge Barcode Delete', 
+  'Delete barcode(s) of subordinate user(s) in a patron merge', 
+  'bool'
+);
+
+INSERT INTO config.org_unit_setting_type 
+( name, label, description, datatype ) VALUES 
+( 'circ.user_merge.deactivate_cards', 
+  'Circ:  Patron Merge Deactivate Card', 
+  'Mark barcode(s) of subordinate user(s) in a patron merge as inactive', 
+  'bool'
+);
+
index 71c45c3..eeb7284 100644 (file)
@@ -1371,6 +1371,9 @@ CREATE OR REPLACE FUNCTION authority.flatten_marc ( TEXT ) RETURNS SETOF authori
 
 use MARC::Record;
 use MARC::File::XML (BinaryEncoding => 'UTF-8');
+use MARC::Charset;
+
+MARC::Charset->assume_unicode(1);
 
 my $xml = shift;
 my $r = MARC::Record->new_from_xml( $xml );
index 6ed4588..2b3540c 100755 (executable)
@@ -96,9 +96,9 @@ cat sql_file_manifest | while read sql_file; do
   export PGHOST PGPORT PGDATABASE PGUSER PGPASSWORD
   # Hide most of the harmless messages that obscure real problems
   if [ -z "$VERBOSE" ]; then
-    psql -f $sql_file 2>&1 | grep -v NOTICE | grep -v "^INSERT"
+    psql -v eg_version=NULL -f $sql_file 2>&1 | grep -v NOTICE | grep -v "^INSERT"
   else
-    psql -f $sql_file
+    psql -v eg_version=NULL -f $sql_file
   fi
   if [ $? != 0 ]; then
     cat <<EOM
diff --git a/Open-ILS/src/sql/Pg/make-db-patch.pl b/Open-ILS/src/sql/Pg/make-db-patch.pl
new file mode 100755 (executable)
index 0000000..ffb1b01
--- /dev/null
@@ -0,0 +1,122 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2011 Equinox Software, Inc.
+# Galen Charlton <gmc@esilibrary.com>
+#
+# Make template for a new DB patch SQL file.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+use strict;
+use warnings;
+
+use Getopt::Long;
+
+my $db_patch_num;
+my $patch_name;
+my @deprecates;
+my @supersedes;
+
+exit_usage() if $#ARGV == -1;
+GetOptions( 
+    'num=i' => \$db_patch_num,
+    'name=s' => \$patch_name,
+    'deprecates=i' => \@deprecates,
+    'supersedes=i' => \@supersedes,
+) or exit_usage();
+
+exit_usage('--num required') unless defined $db_patch_num;
+exit_usage('--name required') unless defined $patch_name;
+
+# pad to four digits
+$db_patch_num = sprintf('%-04.4d', $db_patch_num);
+$_ = sprintf('%-04.4d', $_) foreach @deprecates;
+$_ = sprintf('%-04.4d', $_) foreach @supersedes;
+
+# basic sanity checks
+my @existing = glob("upgrade/$db_patch_num.*");
+if (@existing) {    
+    print "Error: $db_patch_num is already used by $existing[0]\n";
+    exit(1);
+}
+foreach my $dep (@deprecates) {
+    if ($dep gt $db_patch_num) {
+        print "Error: deprecated patch $dep has a higher patch number than $db_patch_num\n";
+        exit(1);
+    }
+}
+foreach my $sup (@supersedes) {
+    if ($sup gt $db_patch_num) {
+        print "Error: superseded patch $sup has a higher patch number than $db_patch_num\n";
+        exit(1);
+    }
+}
+
+my $patch_file_name = "upgrade/$db_patch_num.$patch_name.sql";
+open OUT, '>', $patch_file_name or die "$): cannot open output file $patch_file_name: $!\n";
+
+print OUT <<_HEADER_;
+-- Evergreen DB patch $db_patch_num.$patch_name.sql
+--
+-- FIXME: insert description of change, if needed
+--
+BEGIN;
+
+_HEADER_
+
+if (@deprecates or @supersedes) {
+    my @ins_cols = ('db_patch');
+    my @ins_vals = ("'$db_patch_num'");
+    if (@deprecates) {
+        print OUT "-- Deprecates patch(es): " . join(', ', @deprecates) . "\n"; 
+        push @ins_cols, 'deprecates';
+        push @ins_vals, "ARRAY[" . join(', ', map { "'$_'" } @deprecates) . "]";
+    }
+    if (@supersedes) {
+        print OUT "-- Supersedes patch(es): " . join(', ', @supersedes) . "\n";
+        push @ins_cols, 'supersedes';
+        push @ins_vals, "ARRAY[" . join(', ', map { "'$_'" } @supersedes) . "]";
+    }
+    print OUT "INSERT INTO config.db_patch_dependencies (" .
+              join(', ', @ins_cols) .
+              ")\nVALUES (" .
+              join(', ', @ins_vals) .
+              ");\n";
+}
+
+print OUT <<_FOOTER_;
+
+-- check whether patch can be applied
+SELECT evergreen.update_deps_block_check('$db_patch_num', :eg_version);
+
+-- FIXME: add SQL statements to perform the upgrade
+
+COMMIT;
+_FOOTER_
+
+close OUT;
+print "Created new patch script $patch_file_name -- please go forth and edit.\n";
+
+sub exit_usage {
+    my $msg = shift;
+    print "$msg\n\n" if defined($msg);
+    print <<_HELP_;
+usage: $0 --num <patch_num> --name <patch_name> [--deprecates <num1>] [--supersedes <num2>]
+
+Make template for a DB patch SQL file.
+
+    --num          DB patch number
+    --name         descriptive part of patch filename 
+    --deprecates   patch(es) deprecated by this update
+    --supersedes   patch(es) superseded by this update
+_HELP_
+    exit 0;
+}
diff --git a/Open-ILS/src/sql/Pg/upgrade/0524.data.toggle_unified_volume_copy_editor.sql b/Open-ILS/src/sql/Pg/upgrade/0524.data.toggle_unified_volume_copy_editor.sql
new file mode 100644 (file)
index 0000000..994b180
--- /dev/null
@@ -0,0 +1,13 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0524'); -- phasefx
+
+INSERT into config.org_unit_setting_type
+( name, label, description, datatype ) VALUES
+( 'ui.unified_volume_copy_editor',
+  oils_i18n_gettext( 'ui.unified_volume_copy_editor', 'GUI: Unified Volume/Item Creator/Editor', 'coust', 'label'),
+  oils_i18n_gettext( 'ui.unified_volume_copy_editor', 'If true combines the Volume/Copy Creator and Item Attribute Editor in some instances.', 'coust', 'description'),
+  'bool'
+);
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0525.schema.phys-char-regression.sql b/Open-ILS/src/sql/Pg/upgrade/0525.schema.phys-char-regression.sql
new file mode 100644 (file)
index 0000000..ddbc1f0
--- /dev/null
@@ -0,0 +1,165 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0525'); -- miker
+
+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  m.value INTO attr_value
+                      FROM  biblio.marc21_physical_characteristics(NEW.id) v
+                            JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
+                      WHERE v.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;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0526.schema.upgrade-dep-tracking.sql b/Open-ILS/src/sql/Pg/upgrade/0526.schema.upgrade-dep-tracking.sql
new file mode 100644 (file)
index 0000000..e707053
--- /dev/null
@@ -0,0 +1,100 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0526'); --miker
+
+CREATE TABLE config.db_patch_dependencies (
+  db_patch      TEXT PRIMARY KEY,
+  supersedes    TEXT[],
+  deprecates    TEXT[]
+);
+
+CREATE OR REPLACE FUNCTION evergreen.array_overlap_check (/* field */) RETURNS TRIGGER AS $$
+DECLARE
+    fld     TEXT;
+    cnt     INT;
+BEGIN
+    fld := TG_ARGV[1];
+    EXECUTE 'SELECT COUNT(*) FROM '|| TG_TABLE_SCHEMA ||'.'|| TG_TABLE_NAME ||' WHERE '|| fld ||' && ($1).'|| fld INTO cnt USING NEW;
+    IF cnt > 0 THEN
+        RAISE EXCEPTION 'Cannot insert duplicate array into field % of table %', fld, TG_TABLE_SCHEMA ||'.'|| TG_TABLE_NAME;
+    END IF;
+    RETURN NEW;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE TRIGGER no_overlapping_sups
+    BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
+    FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('supersedes');
+
+CREATE TRIGGER no_overlapping_deps
+    BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
+    FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
+
+ALTER TABLE config.upgrade_log
+    ADD COLUMN applied_to TEXT;
+
+-- List applied db patches that are deprecated by (and block the application of) my_db_patch
+CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_deprecates ( my_db_patch TEXT ) RETURNS SETOF TEXT AS $$
+    SELECT  DISTINCT l.version
+      FROM  config.upgrade_log l
+            JOIN config.db_patch_dependencies d ON (l.version::TEXT[] && d.deprecates)
+      WHERE d.db_patch = $1
+$$ LANGUAGE SQL;
+
+-- List applied db patches that are superseded by (and block the application of) my_db_patch
+CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_supersedes ( my_db_patch TEXT ) RETURNS SETOF TEXT AS $$
+    SELECT  DISTINCT l.version
+      FROM  config.upgrade_log l
+            JOIN config.db_patch_dependencies d ON (l.version::TEXT[] && d.supersedes)
+      WHERE d.db_patch = $1
+$$ LANGUAGE SQL;
+
+-- List applied db patches that deprecates (and block the application of) my_db_patch
+CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_deprecated ( my_db_patch TEXT ) RETURNS TEXT AS $$
+    SELECT  db_patch
+      FROM  config.db_patch_dependencies
+      WHERE ARRAY[$1]::TEXT[] && deprecates
+$$ LANGUAGE SQL;
+
+-- List applied db patches that supersedes (and block the application of) my_db_patch
+CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_superseded ( my_db_patch TEXT ) RETURNS TEXT AS $$
+    SELECT  db_patch
+      FROM  config.db_patch_dependencies
+      WHERE ARRAY[$1]::TEXT[] && supersedes
+$$ LANGUAGE SQL;
+
+-- Make sure that no deprecated or superseded db patches are currently applied
+CREATE OR REPLACE FUNCTION evergreen.upgrade_verify_no_dep_conflicts ( my_db_patch TEXT ) RETURNS BOOL AS $$
+    SELECT  COUNT(*) = 0
+      FROM  (SELECT * FROM evergreen.upgrade_list_applied_deprecates( $1 )
+                UNION
+             SELECT * FROM evergreen.upgrade_list_applied_supersedes( $1 )
+                UNION
+             SELECT * FROM evergreen.upgrade_list_applied_deprecated( $1 )
+                UNION
+             SELECT * FROM evergreen.upgrade_list_applied_superseded( $1 ))x
+$$ LANGUAGE SQL;
+
+-- Raise an exception if there are, in fact, dep/sup confilct
+CREATE OR REPLACE FUNCTION evergreen.upgrade_deps_block_check ( my_db_patch TEXT, my_applied_to TEXT ) RETURNS BOOL AS $$
+BEGIN
+    IF NOT evergreen.upgrade_verify_no_dep_conflicts( my_db_patch ) THEN
+        RAISE EXCEPTION '
+Upgrade script % can not be applied:
+  applied deprecated scripts %
+  applied superseded scripts %
+  deprecated by %
+  superseded by %',
+            my_db_patch,
+            ARRAY_ACUM(evergreen.upgrade_list_applied_deprecates(my_db_patch)),
+            ARRAY_ACUM(evergreen.upgrade_list_applied_supersedes(my_db_patch)),
+            evergreen.upgrade_list_applied_deprecated(my_db_patch),
+            evergreen.upgrade_list_applied_superseded(my_db_patch);
+    END IF;
+
+    INSERT INTO config.upgrade_log (version, applied_to) VALUES (my_db_patch, my_applied_to);
+    RETURN TRUE;
+END;
+$$ LANGUAGE PLPGSQL;
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0527.schema.matrix-bib_level.sql b/Open-ILS/src/sql/Pg/upgrade/0527.schema.matrix-bib_level.sql
new file mode 100644 (file)
index 0000000..ffe5269
--- /dev/null
@@ -0,0 +1,341 @@
+BEGIN;
+
+ALTER TABLE config.circ_matrix_weights ADD COLUMN marc_bib_level NUMERIC(6,2) NOT NULL DEFAULT 0.0;
+
+UPDATE config.circ_matrix_weights
+SET marc_bib_level = marc_vr_format;
+
+ALTER TABLE config.hold_matrix_weights ADD COLUMN marc_bib_level NUMERIC(6, 2) NOT NULL DEFAULT 0.0;
+
+UPDATE config.hold_matrix_weights
+SET marc_bib_level = marc_vr_format;
+
+ALTER TABLE config.circ_matrix_weights ALTER COLUMN marc_bib_level DROP DEFAULT;
+
+ALTER TABLE config.hold_matrix_weights ALTER COLUMN marc_bib_level DROP DEFAULT;
+
+ALTER TABLE config.circ_matrix_matchpoint ADD COLUMN marc_bib_level text;
+
+ALTER TABLE config.hold_matrix_matchpoint ADD COLUMN marc_bib_level text;
+
+CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, item_object asset.copy, user_object actor.usr, renewal BOOL ) RETURNS action.found_circ_matrix_matchpoint AS $func$
+DECLARE
+    cn_object       asset.call_number%ROWTYPE;
+    rec_descriptor  metabib.rec_descriptor%ROWTYPE;
+    cur_matchpoint  config.circ_matrix_matchpoint%ROWTYPE;
+    matchpoint      config.circ_matrix_matchpoint%ROWTYPE;
+    weights         config.circ_matrix_weights%ROWTYPE;
+    user_age        INTERVAL;
+    denominator     NUMERIC(6,2);
+    row_list        INT[];
+    result          action.found_circ_matrix_matchpoint;
+BEGIN
+    -- Assume failure
+    result.success = false;
+
+    -- Fetch useful data
+    SELECT INTO cn_object       * FROM asset.call_number        WHERE id = item_object.call_number;
+    SELECT INTO rec_descriptor  * FROM metabib.rec_descriptor   WHERE record = cn_object.record;
+
+    -- Pre-generate this so we only calc it once
+    IF user_object.dob IS NOT NULL THEN
+        SELECT INTO user_age age(user_object.dob);
+    END IF;
+
+    -- Grab the closest set circ weight setting.
+    SELECT INTO weights cw.*
+      FROM config.weight_assoc wa
+           JOIN config.circ_matrix_weights cw ON (cw.id = wa.circ_weights)
+           JOIN actor.org_unit_ancestors_distance( context_ou ) d ON (wa.org_unit = d.id)
+      WHERE active
+      ORDER BY d.distance
+      LIMIT 1;
+
+    -- No weights? Bad admin! Defaults to handle that anyway.
+    IF weights.id IS NULL THEN
+        weights.grp                 := 11.0;
+        weights.org_unit            := 10.0;
+        weights.circ_modifier       := 5.0;
+        weights.marc_type           := 4.0;
+        weights.marc_form           := 3.0;
+        weights.marc_bib_level      := 2.0;
+        weights.marc_vr_format      := 2.0;
+        weights.copy_circ_lib       := 8.0;
+        weights.copy_owning_lib     := 8.0;
+        weights.user_home_ou        := 8.0;
+        weights.ref_flag            := 1.0;
+        weights.juvenile_flag       := 6.0;
+        weights.is_renewal          := 7.0;
+        weights.usr_age_lower_bound := 0.0;
+        weights.usr_age_upper_bound := 0.0;
+    END IF;
+
+    -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
+    -- If you break your org tree with funky parenting this may be wrong
+    -- Note: This CTE is duplicated in the find_hold_matrix_matchpoint function, and it may be a good idea to split it off to a function
+    -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
+    WITH all_distance(distance) AS (
+            SELECT depth AS distance FROM actor.org_unit_type
+        UNION
+                   SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
+       )
+    SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
+
+    -- Loop over all the potential matchpoints
+    FOR cur_matchpoint IN
+        SELECT m.*
+          FROM  config.circ_matrix_matchpoint m
+                /*LEFT*/ JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.grp = upgad.id
+                /*LEFT*/ JOIN actor.org_unit_ancestors_distance( context_ou ) ctoua ON m.org_unit = ctoua.id
+                LEFT JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) cnoua ON m.copy_owning_lib = cnoua.id
+                LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.copy_circ_lib = iooua.id
+                LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
+          WHERE m.active
+                -- Permission Groups
+             -- AND (m.grp                      IS NULL OR upgad.id IS NOT NULL) -- Optional Permission Group?
+                -- Org Units
+             -- AND (m.org_unit                 IS NULL OR ctoua.id IS NOT NULL) -- Optional Org Unit?
+                AND (m.copy_owning_lib          IS NULL OR cnoua.id IS NOT NULL)
+                AND (m.copy_circ_lib            IS NULL OR iooua.id IS NOT NULL)
+                AND (m.user_home_ou             IS NULL OR uhoua.id IS NOT NULL)
+                -- Circ Type
+                AND (m.is_renewal               IS NULL OR m.is_renewal = renewal)
+                -- Static User Checks
+                AND (m.juvenile_flag            IS NULL OR m.juvenile_flag = user_object.juvenile)
+                AND (m.usr_age_lower_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_lower_bound < user_age))
+                AND (m.usr_age_upper_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_upper_bound > user_age))
+                -- Static Item Checks
+                AND (m.circ_modifier            IS NULL OR m.circ_modifier = item_object.circ_modifier)
+                AND (m.marc_type                IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
+                AND (m.marc_form                IS NULL OR m.marc_form = rec_descriptor.item_form)
+                AND (m.marc_bib_level           IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
+                AND (m.marc_vr_format           IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
+                AND (m.ref_flag                 IS NULL OR m.ref_flag = item_object.ref)
+          ORDER BY
+                -- Permission Groups
+                CASE WHEN upgad.distance        IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0.0 END +
+                -- Org Units
+                CASE WHEN ctoua.distance        IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0.0 END +
+                CASE WHEN cnoua.distance        IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0.0 END +
+                CASE WHEN iooua.distance        IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0.0 END +
+                CASE WHEN uhoua.distance        IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
+                -- Circ Type                    -- Note: 4^x is equiv to 2^(2*x)
+                CASE WHEN m.is_renewal          IS NOT NULL THEN 4^weights.is_renewal ELSE 0.0 END +
+                -- Static User Checks
+                CASE WHEN m.juvenile_flag       IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
+                CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0.0 END +
+                CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0.0 END +
+                -- Static Item Checks
+                CASE WHEN m.circ_modifier       IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
+                CASE WHEN m.marc_type           IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
+                CASE WHEN m.marc_form           IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
+                CASE WHEN m.marc_vr_format      IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
+                CASE WHEN m.ref_flag            IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END DESC,
+                -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
+                -- This prevents "we changed the table order by updating a rule, and we started getting different results"
+                m.id LOOP
+
+        -- Record the full matching row list
+        row_list := row_list || cur_matchpoint.id;
+
+        -- No matchpoint yet?
+        IF matchpoint.id IS NULL THEN
+            -- Take the entire matchpoint as a starting point
+            matchpoint := cur_matchpoint;
+            CONTINUE; -- No need to look at this row any more.
+        END IF;
+
+        -- Incomplete matchpoint?
+        IF matchpoint.circulate IS NULL THEN
+            matchpoint.circulate := cur_matchpoint.circulate;
+        END IF;
+        IF matchpoint.duration_rule IS NULL THEN
+            matchpoint.duration_rule := cur_matchpoint.duration_rule;
+        END IF;
+        IF matchpoint.recurring_fine_rule IS NULL THEN
+            matchpoint.recurring_fine_rule := cur_matchpoint.recurring_fine_rule;
+        END IF;
+        IF matchpoint.max_fine_rule IS NULL THEN
+            matchpoint.max_fine_rule := cur_matchpoint.max_fine_rule;
+        END IF;
+        IF matchpoint.hard_due_date IS NULL THEN
+            matchpoint.hard_due_date := cur_matchpoint.hard_due_date;
+        END IF;
+        IF matchpoint.total_copy_hold_ratio IS NULL THEN
+            matchpoint.total_copy_hold_ratio := cur_matchpoint.total_copy_hold_ratio;
+        END IF;
+        IF matchpoint.available_copy_hold_ratio IS NULL THEN
+            matchpoint.available_copy_hold_ratio := cur_matchpoint.available_copy_hold_ratio;
+        END IF;
+        IF matchpoint.renewals IS NULL THEN
+            matchpoint.renewals := cur_matchpoint.renewals;
+        END IF;
+        IF matchpoint.grace_period IS NULL THEN
+            matchpoint.grace_period := cur_matchpoint.grace_period;
+        END IF;
+    END LOOP;
+
+    -- Check required fields
+    IF matchpoint.circulate             IS NOT NULL AND
+       matchpoint.duration_rule         IS NOT NULL AND
+       matchpoint.recurring_fine_rule   IS NOT NULL AND
+       matchpoint.max_fine_rule         IS NOT NULL THEN
+        -- All there? We have a completed match.
+        result.success := true;
+    END IF;
+
+    -- Include the assembled matchpoint, even if it isn't complete
+    result.matchpoint := matchpoint;
+
+    -- Include (for debugging) the full list of matching rows
+    result.buildrows := row_list;
+
+    -- Hand the result back to caller
+    RETURN result;
+END;
+$func$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
+  RETURNS integer AS
+$func$
+DECLARE
+    requestor_object    actor.usr%ROWTYPE;
+    user_object         actor.usr%ROWTYPE;
+    item_object         asset.copy%ROWTYPE;
+    item_cn_object      asset.call_number%ROWTYPE;
+    rec_descriptor      metabib.rec_descriptor%ROWTYPE;
+    matchpoint          config.hold_matrix_matchpoint%ROWTYPE;
+    weights             config.hold_matrix_weights%ROWTYPE;
+    denominator         NUMERIC(6,2);
+BEGIN
+    SELECT INTO user_object         * FROM actor.usr                WHERE id = match_user;
+    SELECT INTO requestor_object    * FROM actor.usr                WHERE id = match_requestor;
+    SELECT INTO item_object         * FROM asset.copy               WHERE id = match_item;
+    SELECT INTO item_cn_object      * FROM asset.call_number        WHERE id = item_object.call_number;
+    SELECT INTO rec_descriptor      * FROM metabib.rec_descriptor   WHERE record = item_cn_object.record;
+
+    -- The item's owner should probably be the one determining if the item is holdable
+    -- How to decide that is debatable. Decided to default to the circ library (where the item lives)
+    -- This flag will allow for setting it to the owning library (where the call number "lives")
+    PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled;
+
+    -- Grab the closest set circ weight setting.
+    IF NOT FOUND THEN
+        -- Default to circ library
+        SELECT INTO weights hw.*
+          FROM config.weight_assoc wa
+               JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
+               JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id)
+          WHERE active
+          ORDER BY d.distance
+          LIMIT 1;
+    ELSE
+        -- Flag is set, use owning library
+        SELECT INTO weights hw.*
+          FROM config.weight_assoc wa
+               JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
+               JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) d ON (wa.org_unit = d.id)
+          WHERE active
+          ORDER BY d.distance
+          LIMIT 1;
+    END IF;
+
+    -- No weights? Bad admin! Defaults to handle that anyway.
+    IF weights.id IS NULL THEN
+        weights.user_home_ou    := 5.0;
+        weights.request_ou      := 5.0;
+        weights.pickup_ou       := 5.0;
+        weights.item_owning_ou  := 5.0;
+        weights.item_circ_ou    := 5.0;
+        weights.usr_grp         := 7.0;
+        weights.requestor_grp   := 8.0;
+        weights.circ_modifier   := 4.0;
+        weights.marc_type       := 3.0;
+        weights.marc_form       := 2.0;
+        weights.marc_bib_level  := 1.0;
+        weights.marc_vr_format  := 1.0;
+        weights.juvenile_flag   := 4.0;
+        weights.ref_flag        := 0.0;
+    END IF;
+
+    -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
+    -- If you break your org tree with funky parenting this may be wrong
+    -- Note: This CTE is duplicated in the find_circ_matrix_matchpoint function, and it may be a good idea to split it off to a function
+    -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
+    WITH all_distance(distance) AS (
+            SELECT depth AS distance FROM actor.org_unit_type
+        UNION
+            SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
+       )
+    SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
+
+    -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids.
+    -- This may be better implemented as part of the upgrade script?
+    -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set
+    -- Then remove this flag, of course.
+    PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled;
+
+    IF FOUND THEN
+        -- Note: This, to me, is REALLY hacky. I put it in anyway.
+        -- If you can't tell, this is a single call swap on two variables.
+        SELECT INTO user_object.profile, requestor_object.profile
+                    requestor_object.profile, user_object.profile;
+    END IF;
+
+    -- Select the winning matchpoint into the matchpoint variable for returning
+    SELECT INTO matchpoint m.*
+      FROM  config.hold_matrix_matchpoint m
+            /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id
+            LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id
+            LEFT JOIN actor.org_unit_ancestors_distance( pickup_ou ) puoua ON m.pickup_ou = puoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( request_ou ) rqoua ON m.request_ou = rqoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
+      WHERE m.active
+            -- Permission Groups
+         -- AND (m.requestor_grp        IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group?
+            AND (m.usr_grp              IS NULL OR upgad.id IS NOT NULL)
+            -- Org Units
+            AND (m.pickup_ou            IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.request_ou           IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.item_owning_ou       IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.item_circ_ou         IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.user_home_ou         IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match)))
+            -- Static User Checks
+            AND (m.juvenile_flag        IS NULL OR m.juvenile_flag = user_object.juvenile)
+            -- Static Item Checks
+            AND (m.circ_modifier        IS NULL OR m.circ_modifier = item_object.circ_modifier)
+            AND (m.marc_type            IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
+            AND (m.marc_form            IS NULL OR m.marc_form = rec_descriptor.item_form)
+            AND (m.marc_bib_level       IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
+            AND (m.marc_vr_format       IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
+            AND (m.ref_flag             IS NULL OR m.ref_flag = item_object.ref)
+      ORDER BY
+            -- Permission Groups
+            CASE WHEN rpgad.distance    IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0.0 END +
+            CASE WHEN upgad.distance    IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0.0 END +
+            -- Org Units
+            CASE WHEN puoua.distance    IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0.0 END +
+            CASE WHEN rqoua.distance    IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0.0 END +
+            CASE WHEN cnoua.distance    IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0.0 END +
+            CASE WHEN iooua.distance    IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0.0 END +
+            CASE WHEN uhoua.distance    IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
+            -- Static User Checks       -- Note: 4^x is equiv to 2^(2*x)
+            CASE WHEN m.juvenile_flag   IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
+            -- Static Item Checks
+            CASE WHEN m.circ_modifier   IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
+            CASE WHEN m.marc_type       IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
+            CASE WHEN m.marc_form       IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
+            CASE WHEN m.marc_vr_format  IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
+            CASE WHEN m.ref_flag        IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END DESC,
+            -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
+            -- This prevents "we changed the table order by updating a rule, and we started getting different results"
+            m.id;
+
+    -- Return just the ID for now
+    RETURN matchpoint.id;
+END;
+$func$ LANGUAGE 'plpgsql';
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0528.schema.functions_assume_unicode.sql b/Open-ILS/src/sql/Pg/upgrade/0528.schema.functions_assume_unicode.sql
new file mode 100644 (file)
index 0000000..fb19356
--- /dev/null
@@ -0,0 +1,491 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0528'); -- dbs
+
+CREATE OR REPLACE FUNCTION maintain_control_numbers() RETURNS TRIGGER AS $func$
+use strict;
+use MARC::Record;
+use MARC::File::XML (BinaryEncoding => 'UTF-8');
+use MARC::Charset;
+use Encode;
+use Unicode::Normalize;
+
+MARC::Charset->assume_unicode(1);
+
+my $record = MARC::Record->new_from_xml($_TD->{new}{marc});
+my $schema = $_TD->{table_schema};
+my $rec_id = $_TD->{new}{id};
+
+# Short-circuit if maintaining control numbers per MARC21 spec is not enabled
+my $enable = spi_exec_query("SELECT enabled FROM config.global_flag WHERE name = 'cat.maintain_control_numbers'");
+if (!($enable->{processed}) or $enable->{rows}[0]->{enabled} eq 'f') {
+    return;
+}
+
+# Get the control number identifier from an OU setting based on $_TD->{new}{owner}
+my $ou_cni = 'EVRGRN';
+
+my $owner;
+if ($schema eq 'serial') {
+    $owner = $_TD->{new}{owning_lib};
+} else {
+    # are.owner and bre.owner can be null, so fall back to the consortial setting
+    $owner = $_TD->{new}{owner} || 1;
+}
+
+my $ous_rv = spi_exec_query("SELECT value FROM actor.org_unit_ancestor_setting('cat.marc_control_number_identifier', $owner)");
+if ($ous_rv->{processed}) {
+    $ou_cni = $ous_rv->{rows}[0]->{value};
+    $ou_cni =~ s/"//g; # Stupid VIM syntax highlighting"
+} else {
+    # Fall back to the shortname of the OU if there was no OU setting
+    $ous_rv = spi_exec_query("SELECT shortname FROM actor.org_unit WHERE id = $owner");
+    if ($ous_rv->{processed}) {
+        $ou_cni = $ous_rv->{rows}[0]->{shortname};
+    }
+}
+
+my ($create, $munge) = (0, 0);
+
+my @scns = $record->field('035');
+
+foreach my $id_field ('001', '003') {
+    my $spec_value;
+    my @controls = $record->field($id_field);
+
+    if ($id_field eq '001') {
+        $spec_value = $rec_id;
+    } else {
+        $spec_value = $ou_cni;
+    }
+
+    # Create the 001/003 if none exist
+    if (scalar(@controls) == 1) {
+        # Only one field; check to see if we need to munge it
+        unless (grep $_->data() eq $spec_value, @controls) {
+            $munge = 1;
+        }
+    } else {
+        # Delete the other fields, as with more than 1 001/003 we do not know which 003/001 to match
+        foreach my $control (@controls) {
+            unless ($control->data() eq $spec_value) {
+                $record->delete_field($control);
+            }
+        }
+        $record->insert_fields_ordered(MARC::Field->new($id_field, $spec_value));
+        $create = 1;
+    }
+}
+
+# Now, if we need to munge the 001, we will first push the existing 001/003
+# into the 035; but if the record did not have one (and one only) 001 and 003
+# to begin with, skip this process
+if ($munge and not $create) {
+    my $scn = "(" . $record->field('003')->data() . ")" . $record->field('001')->data();
+
+    # Do not create duplicate 035 fields
+    unless (grep $_->subfield('a') eq $scn, @scns) {
+        $record->insert_fields_ordered(MARC::Field->new('035', '', '', 'a' => $scn));
+    }
+}
+
+# Set the 001/003 and update the MARC
+if ($create or $munge) {
+    $record->field('001')->data($rec_id);
+    $record->field('003')->data($ou_cni);
+
+    my $xml = $record->as_xml_record();
+    $xml =~ s/\n//sgo;
+    $xml =~ s/^<\?xml.+\?\s*>//go;
+    $xml =~ s/>\s+</></go;
+    $xml =~ s/\p{Cc}//go;
+
+    # Embed a version of OpenILS::Application::AppUtils->entityize()
+    # to avoid having to set PERL5LIB for PostgreSQL as well
+
+    # If we are going to convert non-ASCII characters to XML entities,
+    # we had better be dealing with a UTF8 string to begin with
+    $xml = decode_utf8($xml);
+
+    $xml = NFC($xml);
+
+    # Convert raw ampersands to entities
+    $xml =~ s/&(?!\S+;)/&amp;/gso;
+
+    # Convert Unicode characters to entities
+    $xml =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
+
+    $xml =~ s/[\x00-\x1f]//go;
+    $_TD->{new}{marc} = $xml;
+
+    return "MODIFY";
+}
+
+return;
+$func$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION authority.generate_overlay_template ( TEXT, BIGINT ) RETURNS TEXT AS $func$
+
+    use MARC::Record;
+    use MARC::File::XML (BinaryEncoding => 'UTF-8');
+    use MARC::Charset;
+
+    MARC::Charset->assume_unicode(1);
+
+    my $xml = shift;
+    my $r = MARC::Record->new_from_xml( $xml );
+
+    return undef unless ($r);
+
+    my $id = shift() || $r->subfield( '901' => 'c' );
+    $id =~ s/^\s*(?:\([^)]+\))?\s*(.+)\s*?$/$1/;
+    return undef unless ($id); # We need an ID!
+
+    my $tmpl = MARC::Record->new();
+    $tmpl->encoding( 'UTF-8' );
+
+    my @rule_fields;
+    for my $field ( $r->field( '1..' ) ) { # Get main entry fields from the authority record
+
+        my $tag = $field->tag;
+        my $i1 = $field->indicator(1);
+        my $i2 = $field->indicator(2);
+        my $sf = join '', map { $_->[0] } $field->subfields;
+        my @data = map { @$_ } $field->subfields;
+
+        my @replace_them;
+
+        # Map the authority field to bib fields it can control.
+        if ($tag >= 100 and $tag <= 111) {       # names
+            @replace_them = map { $tag + $_ } (0, 300, 500, 600, 700);
+        } elsif ($tag eq '130') {                # uniform title
+            @replace_them = qw/130 240 440 730 830/;
+        } elsif ($tag >= 150 and $tag <= 155) {  # subjects
+            @replace_them = ($tag + 500);
+        } elsif ($tag >= 180 and $tag <= 185) {  # floating subdivisions
+            @replace_them = qw/100 400 600 700 800 110 410 610 710 810 111 411 611 711 811 130 240 440 730 830 650 651 655/;
+        } else {
+            next;
+        }
+
+        # Dummy up the bib-side data
+        $tmpl->append_fields(
+            map {
+                MARC::Field->new( $_, $i1, $i2, @data )
+            } @replace_them
+        );
+
+        # Construct some 'replace' rules
+        push @rule_fields, map { $_ . $sf . '[0~\)' .$id . '$]' } @replace_them;
+    }
+
+    # Insert the replace rules into the template
+    $tmpl->append_fields(
+        MARC::Field->new( '905' => ' ' => ' ' => 'r' => join(',', @rule_fields ) )
+    );
+
+    $xml = $tmpl->as_xml_record;
+    $xml =~ s/^<\?.+?\?>$//mo;
+    $xml =~ s/\n//sgo;
+    $xml =~ s/>\s+</></sgo;
+
+    return $xml;
+
+$func$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION vandelay.add_field ( target_xml TEXT, source_xml TEXT, field TEXT, force_add INT ) RETURNS TEXT AS $_$
+
+    use MARC::Record;
+    use MARC::File::XML (BinaryEncoding => 'UTF-8');
+    use MARC::Charset;
+    use strict;
+
+    MARC::Charset->assume_unicode(1);
+
+    my $target_xml = shift;
+    my $source_xml = shift;
+    my $field_spec = shift;
+    my $force_add = shift || 0;
+
+    my $target_r = MARC::Record->new_from_xml( $target_xml );
+    my $source_r = MARC::Record->new_from_xml( $source_xml );
+
+    return $target_xml unless ($target_r && $source_r);
+
+    my @field_list = split(',', $field_spec);
+
+    my %fields;
+    for my $f (@field_list) {
+        $f =~ s/^\s*//; $f =~ s/\s*$//;
+        if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
+            my $field = $1;
+            $field =~ s/\s+//;
+            my $sf = $2;
+            $sf =~ s/\s+//;
+            my $match = $3;
+            $match =~ s/^\s*//; $match =~ s/\s*$//;
+            $fields{$field} = { sf => [ split('', $sf) ] };
+            if ($match) {
+                my ($msf,$mre) = split('~', $match);
+                if (length($msf) > 0 and length($mre) > 0) {
+                    $msf =~ s/^\s*//; $msf =~ s/\s*$//;
+                    $mre =~ s/^\s*//; $mre =~ s/\s*$//;
+                    $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
+                }
+            }
+        }
+    }
+
+    for my $f ( keys %fields) {
+        if ( @{$fields{$f}{sf}} ) {
+            for my $from_field ($source_r->field( $f )) {
+                my @tos = $target_r->field( $f );
+                if (!@tos) {
+                    next if (exists($fields{$f}{match}) and !$force_add);
+                    my @new_fields = map { $_->clone } $source_r->field( $f );
+                    $target_r->insert_fields_ordered( @new_fields );
+                } else {
+                    for my $to_field (@tos) {
+                        if (exists($fields{$f}{match})) {
+                            next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
+                        }
+                        my @new_sf = map { ($_ => $from_field->subfield($_)) } @{$fields{$f}{sf}};
+                        $to_field->add_subfields( @new_sf );
+                    }
+                }
+            }
+        } else {
+            my @new_fields = map { $_->clone } $source_r->field( $f );
+            $target_r->insert_fields_ordered( @new_fields );
+        }
+    }
+
+    $target_xml = $target_r->as_xml_record;
+    $target_xml =~ s/^<\?.+?\?>$//mo;
+    $target_xml =~ s/\n//sgo;
+    $target_xml =~ s/>\s+</></sgo;
+
+    return $target_xml;
+
+$_$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION authority.normalize_heading( TEXT ) RETURNS TEXT AS $func$
+    use strict;
+    use warnings;
+
+    use utf8;
+    use MARC::Record;
+    use MARC::File::XML (BinaryEncoding => 'UTF8');
+    use MARC::Charset;
+    use UUID::Tiny ':std';
+
+    MARC::Charset->assume_unicode(1);
+
+    my $xml = shift() or return undef;
+
+    my $r;
+
+    # Prevent errors in XML parsing from blowing out ungracefully
+    eval {
+        $r = MARC::Record->new_from_xml( $xml );
+        1;
+    } or do {
+       return 'BAD_MARCXML_' . create_uuid_as_string(UUID_MD5, $xml);
+    };
+
+    if (!$r) {
+       return 'BAD_MARCXML_' . create_uuid_as_string(UUID_MD5, $xml);
+    }
+
+    # From http://www.loc.gov/standards/sourcelist/subject.html
+    my $thes_code_map = {
+        a => 'lcsh',
+        b => 'lcshac',
+        c => 'mesh',
+        d => 'nal',
+        k => 'cash',
+        n => 'notapplicable',
+        r => 'aat',
+        s => 'sears',
+        v => 'rvm',
+    };
+
+    # Default to "No attempt to code" if the leader is horribly broken
+    my $fixed_field = $r->field('008');
+    my $thes_char = '|';
+    if ($fixed_field) { 
+        $thes_char = substr($fixed_field->data(), 11, 1) || '|';
+    }
+
+    my $thes_code = 'UNDEFINED';
+
+    if ($thes_char eq 'z') {
+        # Grab the 040 $f per http://www.loc.gov/marc/authority/ad040.html
+        $thes_code = $r->subfield('040', 'f') || 'UNDEFINED';
+    } elsif ($thes_code_map->{$thes_char}) {
+        $thes_code = $thes_code_map->{$thes_char};
+    }
+
+    my $auth_txt = '';
+    my $head = $r->field('1..');
+    if ($head) {
+        # Concatenate all of these subfields together, prefixed by their code
+        # to prevent collisions along the lines of "Fiction, North Carolina"
+        foreach my $sf ($head->subfields()) {
+            $auth_txt .= '‡' . $sf->[0] . ' ' . $sf->[1];
+        }
+    }
+    
+    if ($auth_txt) {
+        my $stmt = spi_prepare('SELECT public.naco_normalize($1) AS norm_text', 'TEXT');
+        my $result = spi_exec_prepared($stmt, $auth_txt);
+        my $norm_txt = $result->{rows}[0]->{norm_text};
+        spi_freeplan($stmt);
+        undef($stmt);
+        return $head->tag() . "_" . $thes_code . " " . $norm_txt;
+    }
+
+    return 'NOHEADING_' . $thes_code . ' ' . create_uuid_as_string(UUID_MD5, $xml);
+$func$ LANGUAGE 'plperlu' IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION vandelay.strip_field ( xml TEXT, field TEXT ) RETURNS TEXT AS $_$
+
+    use MARC::Record;
+    use MARC::File::XML (BinaryEncoding => 'UTF-8');
+    use MARC::Charset;
+    use strict;
+
+    MARC::Charset->assume_unicode(1);
+
+    my $xml = shift;
+    my $r = MARC::Record->new_from_xml( $xml );
+
+    return $xml unless ($r);
+
+    my $field_spec = shift;
+    my @field_list = split(',', $field_spec);
+
+    my %fields;
+    for my $f (@field_list) {
+        $f =~ s/^\s*//; $f =~ s/\s*$//;
+        if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
+            my $field = $1;
+            $field =~ s/\s+//;
+            my $sf = $2;
+            $sf =~ s/\s+//;
+            my $match = $3;
+            $match =~ s/^\s*//; $match =~ s/\s*$//;
+            $fields{$field} = { sf => [ split('', $sf) ] };
+            if ($match) {
+                my ($msf,$mre) = split('~', $match);
+                if (length($msf) > 0 and length($mre) > 0) {
+                    $msf =~ s/^\s*//; $msf =~ s/\s*$//;
+                    $mre =~ s/^\s*//; $mre =~ s/\s*$//;
+                    $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
+                }
+            }
+        }
+    }
+
+    for my $f ( keys %fields) {
+        for my $to_field ($r->field( $f )) {
+            if (exists($fields{$f}{match})) {
+                next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
+            }
+
+            if ( @{$fields{$f}{sf}} ) {
+                $to_field->delete_subfield(code => $fields{$f}{sf});
+            } else {
+                $r->delete_field( $to_field );
+            }
+        }
+    }
+
+    $xml = $r->as_xml_record;
+    $xml =~ s/^<\?.+?\?>$//mo;
+    $xml =~ s/\n//sgo;
+    $xml =~ s/>\s+</></sgo;
+
+    return $xml;
+
+$_$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION biblio.flatten_marc ( TEXT ) RETURNS SETOF metabib.full_rec AS $func$
+
+use MARC::Record;
+use MARC::File::XML (BinaryEncoding => 'UTF-8');
+use MARC::Charset;
+
+MARC::Charset->assume_unicode(1);
+
+my $xml = shift;
+my $r = MARC::Record->new_from_xml( $xml );
+
+return_next( { tag => 'LDR', value => $r->leader } );
+
+for my $f ( $r->fields ) {
+       if ($f->is_control_field) {
+               return_next({ tag => $f->tag, value => $f->data });
+       } else {
+               for my $s ($f->subfields) {
+                       return_next({
+                               tag      => $f->tag,
+                               ind1     => $f->indicator(1),
+                               ind2     => $f->indicator(2),
+                               subfield => $s->[0],
+                               value    => $s->[1]
+                       });
+
+                       if ( $f->tag eq '245' and $s->[0] eq 'a' ) {
+                               my $trim = $f->indicator(2) || 0;
+                               return_next({
+                                       tag      => 'tnf',
+                                       ind1     => $f->indicator(1),
+                                       ind2     => $f->indicator(2),
+                                       subfield => 'a',
+                                       value    => substr( $s->[1], $trim )
+                               });
+                       }
+               }
+       }
+}
+
+return undef;
+
+$func$ LANGUAGE PLPERLU;
+
+CREATE OR REPLACE FUNCTION authority.flatten_marc ( TEXT ) RETURNS SETOF authority.full_rec AS $func$
+
+use MARC::Record;
+use MARC::File::XML (BinaryEncoding => 'UTF-8');
+use MARC::Charset;
+
+MARC::Charset->assume_unicode(1);
+
+my $xml = shift;
+my $r = MARC::Record->new_from_xml( $xml );
+
+return_next( { tag => 'LDR', value => $r->leader } );
+
+for my $f ( $r->fields ) {
+    if ($f->is_control_field) {
+        return_next({ tag => $f->tag, value => $f->data });
+    } else {
+        for my $s ($f->subfields) {
+            return_next({
+                tag      => $f->tag,
+                ind1     => $f->indicator(1),
+                ind2     => $f->indicator(2),
+                subfield => $s->[0],
+                value    => $s->[1]
+            });
+
+        }
+    }
+}
+
+return undef;
+
+$func$ LANGUAGE PLPERLU;
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0529.data.merge_user-ou_settings.sql b/Open-ILS/src/sql/Pg/upgrade/0529.data.merge_user-ou_settings.sql
new file mode 100644 (file)
index 0000000..6bc688f
--- /dev/null
@@ -0,0 +1,29 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0529');
+
+INSERT INTO config.org_unit_setting_type 
+( name, label, description, datatype ) VALUES 
+( 'circ.user_merge.delete_addresses', 
+  'Circ:  Patron Merge Address Delete', 
+  'Delete address(es) of subordinate user(s) in a patron merge', 
+   'bool'
+);
+
+INSERT INTO config.org_unit_setting_type 
+( name, label, description, datatype ) VALUES 
+( 'circ.user_merge.delete_cards', 
+  'Circ: Patron Merge Barcode Delete', 
+  'Delete barcode(s) of subordinate user(s) in a patron merge', 
+  'bool'
+);
+
+INSERT INTO config.org_unit_setting_type 
+( name, label, description, datatype ) VALUES 
+( 'circ.user_merge.deactivate_cards', 
+  'Circ:  Patron Merge Deactivate Card', 
+  'Mark barcode(s) of subordinate user(s) in a patron merge as inactive', 
+  'bool'
+);\r
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0530.schema.actor-usr-index-phone-fields-more.sql b/Open-ILS/src/sql/Pg/upgrade/0530.schema.actor-usr-index-phone-fields-more.sql
new file mode 100644 (file)
index 0000000..c8dd6d3
--- /dev/null
@@ -0,0 +1,14 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0530'); -- senator
+
+CREATE INDEX actor_usr_day_phone_idx_numeric ON actor.usr USING BTREE 
+    (evergreen.lowercase(REGEXP_REPLACE(day_phone, '[^0-9]', '', 'g')));
+
+CREATE INDEX actor_usr_evening_phone_idx_numeric ON actor.usr USING BTREE 
+    (evergreen.lowercase(REGEXP_REPLACE(evening_phone, '[^0-9]', '', 'g')));
+
+CREATE INDEX actor_usr_other_phone_idx_numeric ON actor.usr USING BTREE 
+    (evergreen.lowercase(REGEXP_REPLACE(other_phone, '[^0-9]', '', 'g')));
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0531.schema.auditor_affixes.sql b/Open-ILS/src/sql/Pg/upgrade/0531.schema.auditor_affixes.sql
new file mode 100644 (file)
index 0000000..b8d010f
--- /dev/null
@@ -0,0 +1,9 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0531'); --gmc
+
+ALTER TABLE auditor.asset_call_number_history
+    ADD COLUMN prefix INT,
+    ADD COLUMN suffix INT;
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0532.schema.fix_copy_count_funcs.sql b/Open-ILS/src/sql/Pg/upgrade/0532.schema.fix_copy_count_funcs.sql
new file mode 100644 (file)
index 0000000..06c3922
--- /dev/null
@@ -0,0 +1,131 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0532'); --gmc
+
+CREATE OR REPLACE FUNCTION asset.opac_ou_record_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+    ans RECORD;
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+
+    FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
+        RETURN QUERY
+        SELECT  ans.depth,
+                ans.id,
+                COUNT( av.id ),
+                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                COUNT( av.id ),
+                trans
+          FROM  
+                actor.org_unit_descendants(ans.id) d
+                JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
+                JOIN asset.copy cp ON (cp.id = av.copy_id)
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+    ans RECORD;
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+
+    FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
+        RETURN QUERY
+        SELECT  -1,
+                ans.id,
+                COUNT( av.id ),
+                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                COUNT( av.id ),
+                trans
+          FROM
+                actor.org_unit_descendants(ans.id) d
+                JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
+                JOIN asset.copy cp ON (cp.id = av.copy_id)
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;   
+                
+    RETURN;     
+END;            
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION asset.opac_ou_metarecord_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+    ans RECORD;
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+
+    FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
+        RETURN QUERY
+        SELECT  ans.depth,
+                ans.id,
+                COUNT( av.id ),
+                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                COUNT( av.id ),
+                trans
+          FROM  
+                actor.org_unit_descendants(ans.id) d
+                JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
+                JOIN asset.copy cp ON (cp.id = av.copy_id)
+                JOIN metabib.metarecord_source_map m ON (m.source = av.record)
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION asset.opac_lasso_metarecord_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+    ans RECORD;
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+
+    FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
+        RETURN QUERY
+        SELECT  -1,
+                ans.id,
+                COUNT( av.id ),
+                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                COUNT( av.id ),
+                trans
+          FROM
+                actor.org_unit_descendants(ans.id) d
+                JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
+                JOIN asset.copy cp ON (cp.id = av.copy_id)
+                JOIN metabib.metarecord_source_map m ON (m.source = av.record)
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;   
+                
+    RETURN;     
+END;            
+$f$ LANGUAGE PLPGSQL;
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0533.schema.fix_age_circ.sql b/Open-ILS/src/sql/Pg/upgrade/0533.schema.fix_age_circ.sql
new file mode 100644 (file)
index 0000000..49986d4
--- /dev/null
@@ -0,0 +1,44 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0533'); -- gmc
+
+CREATE OR REPLACE FUNCTION action.age_circ_on_delete () RETURNS TRIGGER AS $$
+DECLARE
+found char := 'N';
+BEGIN
+
+    -- If there are any renewals for this circulation, don't archive or delete
+    -- it yet.   We'll do so later, when we archive and delete the renewals.
+
+    SELECT 'Y' INTO found
+    FROM action.circulation
+    WHERE parent_circ = OLD.id
+    LIMIT 1;
+
+    IF found = 'Y' THEN
+        RETURN NULL;  -- don't delete
+       END IF;
+
+    -- Archive a copy of the old row to action.aged_circulation
+
+    INSERT INTO action.aged_circulation
+        (id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
+        copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
+        circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
+        stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
+        max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
+        max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ)
+      SELECT
+        id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
+        copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
+        circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
+        stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
+        max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
+        max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
+        FROM action.all_circulation WHERE id = OLD.id;
+
+    RETURN OLD;
+END;
+$$ LANGUAGE 'plpgsql';
+
+COMMIT;
@@ -1,5 +1,5 @@
 #!/usr/bin/perl
-# Copyright (C) 2010 Laurentian University
+# Copyright (C) 2010-2011 Laurentian University
 # Author: Dan Scott <dscott@laurentian.ca>
 #
 # This program is free software; you can redistribute it and/or
@@ -19,17 +19,21 @@ use DBI;
 use Getopt::Long;
 use MARC::Record;
 use MARC::File::XML (BinaryEncoding => 'UTF-8');
+use MARC::Charset;
 use OpenSRF::System;
 use OpenILS::Utils::Fieldmapper;
 use OpenSRF::Utils::SettingsClient;
+use OpenSRF::EX qw/:try/;
 use Encode;
 use Unicode::Normalize;
 use OpenILS::Application::AppUtils;
 use Data::Dumper;
 use Pod::Usage qw/ pod2usage /;
 
+MARC::Charset->assume_unicode(1);
+
 my ($start_id, $end_id);
-my $bootstrap = '/openils/conf/opensrf_core.xml';
+my $bootstrap = '@sysconfdir@/opensrf_core.xml';
 my @records;
 
 my %options;
@@ -53,12 +57,12 @@ Fieldmapper->import(IDL => OpenSRF::Utils::SettingsClient->new->config_value("ID
 use OpenILS::Utils::CStoreEditor;
 OpenILS::Utils::CStoreEditor::init();
 
-my $editor = OpenILS::Utils::CStoreEditor->new;
+my $e = OpenILS::Utils::CStoreEditor->new;
 my $undeleted;
 if ($options{all}) {
     # get a list of all non-deleted records from Evergreen
     # open-ils.cstore open-ils.cstore.direct.biblio.record_entry.id_list.atomic {"deleted":"f"}
-    $undeleted = $editor->request( 
+    $undeleted = $e->request( 
         'open-ils.cstore.direct.biblio.record_entry.id_list.atomic', 
         [{deleted => 'f'}, {id => { '>' => 0}}]
     );
@@ -339,7 +343,6 @@ my %controllees = (
 foreach my $rec_id (@records) {
     # print "$rec_id\n";
 
-    my $e = OpenILS::Utils::CStoreEditor->new(xact=>1);
     # State variable; was the record changed?
     my $changed;
 
@@ -348,88 +351,101 @@ foreach my $rec_id (@records) {
     next unless $record;
     # print Dumper($record);
 
-    my $marc = MARC::Record->new_from_xml($record->marc());
-
-    # get the list of controlled fields
-    my @c_fields = keys %controllees;
-
-    foreach my $c_tag (@c_fields) {
-        my @c_subfields = keys %{$controllees{"$c_tag"}};
-        # print "Field: $field subfields: ";
-        # foreach (@subfields) { print "$_ "; }
-
-        # Get the MARCXML from the record and check for controlled fields/subfields
-        my @bib_fields = ($marc->field($c_tag));
-        foreach my $bib_field (@bib_fields) {
-            # print $_->as_formatted(); 
-            my %match_subfields;
-            my $match_tag;
-            my @searches;
-            foreach my $c_subfield (@c_subfields) {
-                my $sf = $bib_field->subfield($c_subfield);
-                if ($sf) {
-                    # Give me the first element of the list of authority controlling tags for this subfield
-                    # XXX Will we need to support more than one controlling tag per subfield? Probably. That
-                    # will suck. Oh well, leave that up to Ole to implement.
-                    $match_subfields{$c_subfield} = (keys %{$controllees{$c_tag}{$c_subfield}})[0];
-                    $match_tag = $match_subfields{$c_subfield};
-                    push @searches, {term => $sf, subfield => $c_subfield};
+    try {
+        my $marc = MARC::Record->new_from_xml($record->marc());
+
+        # get the list of controlled fields
+        my @c_fields = keys %controllees;
+
+        foreach my $c_tag (@c_fields) {
+            my @c_subfields = keys %{$controllees{"$c_tag"}};
+            # print "Field: $field subfields: ";
+            # foreach (@subfields) { print "$_ "; }
+
+            # Get the MARCXML from the record and check for controlled fields/subfields
+            my @bib_fields = ($marc->field($c_tag));
+            foreach my $bib_field (@bib_fields) {
+                # print $_->as_formatted(); 
+                my %match_subfields;
+                my $match_tag;
+                my @searches;
+                foreach my $c_subfield (@c_subfields) {
+                    my $sf = $bib_field->subfield($c_subfield);
+                    if ($sf) {
+                        # Give me the first element of the list of authority controlling tags for this subfield
+                        # XXX Will we need to support more than one controlling tag per subfield? Probably. That
+                        # will suck. Oh well, leave that up to Ole to implement.
+                        $match_subfields{$c_subfield} = (keys %{$controllees{$c_tag}{$c_subfield}})[0];
+                        $match_tag = $match_subfields{$c_subfield};
+                        push @searches, {term => $sf, subfield => $c_subfield};
+                    }
                 }
-            }
-            # print Dumper(\%match_subfields);
-            next if !$match_tag;
+                # print Dumper(\%match_subfields);
+                next if !$match_tag;
 
-            my @tags = ($match_tag);
+                my @tags = ($match_tag);
 
-            # print "Controlling tag: $c_tag and match tag $match_tag\n";
-            # print Dumper(\@tags, \@searches);
+                # print "Controlling tag: $c_tag and match tag $match_tag\n";
+                # print Dumper(\@tags, \@searches);
 
-            # Now we've built up a complete set of matching controlled
-            # subfields for this particular field; let's check to see if
-            # we have a matching authority record
-            my $session = OpenSRF::AppSession->create("open-ils.search");
-            my $validates = $session->request("open-ils.search.authority.validate.tag.id_list", 
-                "tags", \@tags, "searches", \@searches
-            )->gather();
-            $session->disconnect();
+                # Now we've built up a complete set of matching controlled
+                # subfields for this particular field; let's check to see if
+                # we have a matching authority record
+                my $session = OpenSRF::AppSession->create("open-ils.search");
+                my $validates = $session->request("open-ils.search.authority.validate.tag.id_list", 
+                    "tags", \@tags, "searches", \@searches
+                )->gather();
+                $session->disconnect();
 
-            # print Dumper($validates);
+                # print Dumper($validates);
 
-            if (scalar(@$validates) == 0) {
-                next;
-            }
+                # Protect against failed (error condition) search request
+                if (!$validates) {
+                    print STDERR "Search for matching authority failed; record # $rec_id\n";
+                    next;
+                }
 
-            # Iterate through the returned authority record IDs to delete any
-            # matching $0 subfields already in the bib record
-            foreach my $auth_zero (@$validates) {
-                $bib_field->delete_subfield(code => '0', match => qr/\)$auth_zero$/);
-            }
+                if (scalar(@$validates) == 0) {
+                    next;
+                }
 
-            # Okay, we have a matching authority control; time to
-            # add the magical subfield 0. Use the first returned auth
-            # record as a match.
-            my $auth_id = @$validates[0];
-            my $auth_rec = $e->retrieve_authority_record_entry($auth_id);
-            my $auth_marc = MARC::Record->new_from_xml($auth_rec->marc());
-            my $cni = $auth_marc->field('003')->data();
-            
-            $bib_field->add_subfields('0' => "($cni)$auth_id");
-            $changed = 1;
+                # Iterate through the returned authority record IDs to delete any
+                # matching $0 subfields already in the bib record
+                foreach my $auth_zero (@$validates) {
+                    $bib_field->delete_subfield(code => '0', match => qr/\)$auth_zero$/);
+                }
+
+                # Okay, we have a matching authority control; time to
+                # add the magical subfield 0. Use the first returned auth
+                # record as a match.
+                my $auth_id = @$validates[0];
+                my $auth_rec = $e->retrieve_authority_record_entry($auth_id);
+                my $auth_marc = MARC::Record->new_from_xml($auth_rec->marc());
+                my $cni = $auth_marc->field('003')->data();
+                
+                $bib_field->add_subfields('0' => "($cni)$auth_id");
+                $changed = 1;
+            }
         }
+        if ($changed) {
+            my $editor = OpenILS::Utils::CStoreEditor->new(xact=>1);
+            # print $marc->as_formatted();
+            my $xml = $marc->as_xml_record();
+            $xml =~ s/\n//sgo;
+            $xml =~ s/^<\?xml.+\?\s*>//go;
+            $xml =~ s/>\s+</></go;
+            $xml =~ s/\p{Cc}//go;
+            $xml = OpenILS::Application::AppUtils->entityize($xml);
+
+            $record->marc($xml);
+            $editor->update_biblio_record_entry($record);
+            $editor->commit();
+        }
+    } otherwise {
+        my $err = shift;
+        print STDERR "\nRecord # $rec_id : $err\n";
+        import MARC::File::XML; # reset SAX parser so that one bad record doesn't kill the entire process
     }
-    if ($changed) {
-        # print $marc->as_formatted();
-        my $xml = $marc->as_xml_record();
-        $xml =~ s/\n//sgo;
-        $xml =~ s/^<\?xml.+\?\s*>//go;
-        $xml =~ s/>\s+</></go;
-        $xml =~ s/\p{Cc}//go;
-        $xml = OpenILS::Application::AppUtils->entityize($xml);
-
-        $record->marc($xml);
-        $e->update_biblio_record_entry($record);
-    }
-    $e->commit();
 }
 
 __END__
@@ -490,7 +506,7 @@ table identifying the record ID, field, and subfield(s) that were not controlled
 =item * B<-c> I<config-file>, B<--configuration>=I<config-file>
 
 Specifies the OpenSRF configuration file used to connect to the OpenSRF router.
-Defaults to F</openils/conf/opensrf_core.xml>
+Defaults to F<@sysconfdir@/opensrf_core.xml>
 
 =item * B<-r> I<record-ID>, B<--record>=I<record-ID>
 
index 69526af..4e53914 100755 (executable)
@@ -48,9 +48,11 @@ my %defaults = (
     'user=s'        => 'admin',
     'password=s'    => '',
     'tempdir=s'     => '',
+    'spoolfile=s'   => '',
     'nolockfile'    => 1,
     'queue=i'       => 1,
     'noqueue'       => 0,
+    'nodaemon'      => 0,
     'wait=i'        => 5,
     'import-by-queue' => 0
 );
@@ -154,10 +156,16 @@ sub xml_import {
 
 sub old_process_batch_data {
     my $data = shift or $logger->error("process_batch_data called without any data");
+    my $isfile = shift;
     $data or return;
 
     my $handle;
-    open $handle, '<', \$data; 
+    if ($isfile) {
+        $handle = $data;
+    } else {
+        open $handle, '<', \$data; 
+    }
+
     my $batch = MARC::Batch->new('USMARC', $handle);
     $batch->strict_off;
 
@@ -315,13 +323,19 @@ sub bib_queue_import {
 
 sub process_batch_data {
     my $data = shift or $logger->error("process_batch_data called without any data");
+    my $isfile = shift;
     $data or return;
 
     $vl_ses = OpenSRF::AppSession->create('open-ils.vandelay');
 
-    my ($handle, $tempfile) = File::Temp->tempfile("$0_XXXX", DIR => $tempdir) or die "Cannot write tempfile in $tempdir";
-    print $handle $data;
-    close $handle;
+    my ($handle, $tempfile);
+    if (!$isfile) {
+        ($handle, $tempfile) = File::Temp->tempfile("$0_XXXX", DIR => $tempdir) or die "Cannot write tempfile in $tempdir";
+        print $handle $data;
+        close $handle;
+    } else {
+        $tempfile = $data;
+    }
        
     $logger->info("Calling process_spool on tempfile $tempfile (queue: $queue_id; source: $bib_source)");
     my $rec_ids = process_spool($tempfile);
@@ -358,12 +372,15 @@ sub process_request {   # The core Net::Server method
         exit;
     }
 
-    my $data;
+    my $data = '';
     eval {
         local $SIG{ALRM} = sub { die "alarm\n" };
         alarm $wait_time; # prevent accidental tie ups of backend processes
         local $/ = "\x1D"; # MARC record separator
-        $data = <STDIN>;
+        while (my $newline = <STDIN>) {
+            $data .= $newline;
+            alarm $wait_time; # prevent accidental tie ups of backend processes
+        }
         alarm 0;
     };
 
@@ -400,6 +417,44 @@ sub process_request {   # The core Net::Server method
     clear_auth_token(); # logout
 }
 
+sub standalone_process_request {   # The command line version
+    my $file = shift;
+    
+    $logger->info("stream parser received file processing request for $file");
+
+    my $ph = OpenSRF::Transport::PeerHandle->retrieve;
+    if(!$ph->flush_socket()) {
+        $logger->error("We received a request, bu we are no longer connected to opensrf.  ".
+            "Exiting and dropping request for $file");
+        exit;
+    }
+
+    my ($imported, $failed) = (0, 0);
+
+    new_auth_token(); # login
+
+    if ($real_opts->{noqueue}) {
+        ($imported, $failed) = old_process_batch_data($file, 1);
+    } else {
+        ($imported, $failed) = process_batch_data($file, 1);
+    }
+
+    my $profile = (!$merge_profile) ? '' :
+        $apputils->simplereq(
+            'open-ils.pcrud', 
+            'open-ils.pcrud.retrieve.vmp', 
+            $authtoken, 
+            $merge_profile)->name;
+
+    my $msg = '';
+    $msg .= "Successfully imported $imported records using merge profile '$profile'\n" if $imported;
+    $msg .= "Failed to import $failed records\n" if $failed;
+    $msg .= "\x00";
+    print $msg;
+
+    clear_auth_token(); # logout
+}
+
 
 # the authtoken will timeout after the configured inactivity period.
 # When that happens, get a new one.
@@ -420,8 +475,16 @@ sub clear_auth_token {
 ##### MAIN ######
 
 osrf_connect($osrf_config);
-print "Calling Net::Server run ", (@ARGV ? "with command-line options: " . join(' ', @ARGV) : ''), "\n";
-__PACKAGE__->run(conf_file => $conf_file);
+if ($real_opts->{nodaemon}) {
+    if (!$real_opts->{spoolfile}) {
+        print " --nodaemon mode requested, but no --spoolfile supplied!\n";
+        exit;
+    }
+    standalone_process_request($real_opts->{spoolfile});
+} else {
+    print "Calling Net::Server run ", (@ARGV ? "with command-line options: " . join(' ', @ARGV) : ''), "\n";
+    __PACKAGE__->run(conf_file => $conf_file);
+}
 
 __END__
 
@@ -442,7 +505,7 @@ underlying L<Net::Server> options.
 
 Note: this script has to be run in the same directory as B<oils_header.pl>.
 
-Typical execution will include a trailing C<&> to run in the background.
+Typical server-style execution will include a trailing C<&> to run in the background.
 
 =head1 DESCRIPTION
 
@@ -459,6 +522,8 @@ The only required option is --password
  --tempdir          =</temp/dir/>   default: from L<opensrf.conf> <open-ils.vandelay/app_settings/databases/importer>
  --source           =<i>            default: 1
  --import-by-queue  =<i>            default: 0
+ --spoolfile        =<import_file>  default: NONE      File to import in --nodaemon mode
+ --nodaemon                         default: OFF       When used with --spoolfile, turns off Net::Server mode and runs this utility in the foreground
 
 
 =head2 Old style: --noqueue and associated options
@@ -507,6 +572,10 @@ or via the command line to restrict access as necessary.
     admin open-ils connexion --port 5555 --min_servers 2 \
     --max_servers=20 --log_file=/openils/var/log/marc_net_importer.log &
 
+./marc_stream_importer.pl  \
+    admin open-ils connexion --port 5555 --min_servers 2 \
+    --max_servers=20 --log_file=/openils/var/log/marc_net_importer.log &
+
 =head1 SEE ALSO
 
 L<Net::Server::PreFork>, L<marc_stream_importer.conf>
@@ -515,5 +584,6 @@ L<Net::Server::PreFork>, L<marc_stream_importer.conf>
 
     Bill Erickson <erickson@esilibrary.com>
     Joe Atzberger <jatzberger@esilibrary.com>
+    Mike Rylander <miker@esilibrary.com> (nodaemon+spoolfile mode)
 
 =cut
index 664976e..7844e17 100644 (file)
@@ -214,6 +214,7 @@ span[name="notes_alert_flag"] {color: #c00;font-weight: bold;font-size: 110%;mar
 .acq-link-invoice-dialog td,.acq-link-invoice-dialog th {padding-top: 10px;}
 .acq-invoice-paid-col {background : #E0E0E0; text-align: center;}
 .acq-invoice-center-col { text-align: center; }
+.acq-invoice-money { width: 7em; }
 
 .acq-lineitem-summary { font-weight: bold; }
 .acq-lineitem-summary-extra { padding-left: 10px; }
index a52c6c7..4d0ec80 100644 (file)
@@ -181,7 +181,14 @@ openils.acq.Lineitem.fetchAndRender = function(liId, args, callback) {
                         liLink,
                         (po) ? 'foo' : '', // forces class='hiddenfoo' i.e. not hidden
                         (pl) ? 'foo' : '', // ditto
-                    ]
+                    ],
+                    function(str) {
+                        // prevent long titles from filling up the page
+                        var truncSize = 100;
+                        if(str.length > truncSize)
+                            str = str.substring(0, truncSize) + '...';
+                        return str;
+                    }
                 );
 
                 callback(lineitem, displayString);
index ab1150a..b16af89 100644 (file)
@@ -9,5 +9,26 @@
     "DUPE_PATRON_ADDR" : "Found ${0} patron(s) with the same address",
     "REPLACED_ADDRESS" : "<div>Replaces address <b>${0}</b><br/> ${1} ${2}<br/> ${3}, ${4} ${5}</div>",
     "INVALID_FORM" : "Form is invalid.  Please edit and try again.",
-    "EXAMPLE" : "Example: "
+    "EXAMPLE" : "Example: ",
+    "REPLACE_BARCODE" : "Replace Barcode",
+    "BARCODE_IN_USE" : "Barcode is already in use",
+    "SEE_ALL" : "See All",
+    "DUPE_USERNAME" : "Username is already in use",
+    "RESET_PASSWORD" : "Reset Password",
+    "VERIFY_PASSWORD" : "Verify Password",
+    "PARENT_OR_GUARDIAN" : "Parent/Guardian",
+    "USER_SETTINGS" : "User Settings",
+    "ADDRESS_HEADER" : "Address",
+    "ADDRESS_MAILING" : "Mailing",
+    "ADDRESS_BILLING" : "Billing",
+    "ADDRESS_PENDING" : "This is a pending address: ",
+    "ADDRESS_APPROVE" : "Approve Address",
+    "ADDRESS_OWNED" : "This address is owned by another user: ",
+    "ADDRESS_NEW" : "New Address",
+    "STAT_CATS" : "Statistical Categories",
+    "SAVE" : "Save",
+    "SAVE_CLONE" : "Save &amp; Clone",
+    "SHOW_REQUIRED" : "Show Only Required Fields",
+    "SHOW_SUGGESTED" : "Show Suggested Fields",
+    "SHOW_ALL" : "Show All Fields"
 }
index 577e93c..faa68ad 100644 (file)
@@ -1,42 +1,42 @@
 {
-    and : 'AND',
-    or  : 'OR',
-    more  : 'More...',
-    less  : '...Less',
-    classed_searches  : 'Classed Searches',
-    faceted_searches  : 'Facets',
-    filters  : 'Filters',
-    modifiers  : 'Modifiers',
-    new_facet  : 'New Facet',
-    new_filter  : 'New Filter',
-    new_modifier  : 'New Modifier',
-    remove  : 'Remove',
-    advanced  : 'Advanced',
-    basic  : 'Basic',
-    perform_search  : 'Go!',
-    contains : 'Contains',
-    notcontains : 'Does Not Contain',
-    exact : 'Exactly Matches',
-    modifier_default : '-- Select a Modifier --',
-    available : 'Available',
-    descending : 'Sort Descending',
-    ascending : 'Sort Ascending',
-    metabib : 'Metarecord Search',
-    staff : 'Staff Search',
-    filter_default : '-- Select a Filter --',
-    site : 'Site',
-    depth : 'Search Depth',
-    sort : 'Sort Axis',
-    statuses : 'Statuses',
-    audience : 'Audience',
-    before : 'Published Before',
-    after : 'Published After',
-    between : 'Published Between',
-    during : 'In Publication',
-    item_form : 'Form',
-    item_type : 'Type',
-    format : 'Type and Form',
-    vr_format : 'Videorecording Format',
-    lit_form : 'Literary Form',
-    bib_level : 'Bibliographic Level'
+    "and" : "AND",
+    "or" : "OR",
+    "more" : "More...",
+    "less" : "...Less",
+    "classed_searches" : "Classed Searches",
+    "faceted_searches" : "Facets",
+    "filters" : "Filters",
+    "modifiers" : "Modifiers",
+    "new_facet" : "New Facet",
+    "new_filter" : "New Filter",
+    "new_modifier" : "New Modifier",
+    "remove" : "Remove",
+    "advanced" : "Advanced",
+    "basic" : "Basic",
+    "perform_search" : "Go!",
+    "contains" : "Contains",
+    "notcontains" : "Does Not Contain",
+    "exact" : "Exactly Matches",
+    "modifier_default" : "-- Select a Modifier --",
+    "available" : "Available",
+    "descending" : "Sort Descending",
+    "ascending" : "Sort Ascending",
+    "metabib" : "Metarecord Search",
+    "staff" : "Staff Search",
+    "filter_default" : "-- Select a Filter --",
+    "site" : "Site",
+    "depth" : "Search Depth",
+    "sort" : "Sort Axis",
+    "statuses" : "Statuses",
+    "audience" : "Audience",
+    "before" : "Published Before",
+    "after" : "Published After",
+    "between" : "Published Between",
+    "during" : "In Publication",
+    "item_form" : "Form",
+    "item_type" : "Type",
+    "format" : "Type and Form",
+    "vr_format" : "Videorecording Format",
+    "lit_form" : "Literary Form",
+    "bib_level" : "Bibliographic Level"
 }
index 38465b9..e7068b1 100644 (file)
@@ -133,9 +133,22 @@ function doAttachLi() {
 
     //var invoiceArgs = {provider : lineitem.provider(), shipper : lineitem.provider()}; 
     if(cgi.param('create')) {
-        var invoiceArgs = {};
-        invoicePane = drawInvoicePane(dojo.byId('acq-view-invoice-div'), null, invoiceArgs);
+
+        fieldmapper.standardRequest(
+            ['open-ils.acq', 'open-ils.acq.lineitem.retrieve.authoritative'],
+            {
+                params : [openils.User.authtoken, attachLi, {clear_marc:1}],
+                oncomplete : function(r) {
+                    var li = openils.Util.readResponse(r);
+                    invoicePane = drawInvoicePane(
+                        dojo.byId('acq-view-invoice-div'), null, 
+                        {provider : li.provider(), shipper : li.provider()}
+                    );
+                }
+            }
+        );
     }
+
     var entry = new fieldmapper.acqie();
     entry.id(virtualId--);
     entry.isnew(true);
@@ -283,7 +296,7 @@ function addInvoiceItem(item) {
             if(field == 'title' || field == 'author') {
                 //args = {style : 'width:10em'};
             } else if(field == 'cost_billed' || field == 'amount_paid') {
-                args = {required : true, style : 'width: 6em'};
+                args = {required : true, style : 'width: 8em'};
             }
             registerWidget(
                 item,
@@ -430,6 +443,7 @@ function addInvoiceEntry(entry) {
                 ['inv_item_count', 'phys_item_count', 'cost_billed', 'amount_paid'],
                 function(field) {
                     var dijitArgs = {required : true, constraints : {min: 0}, style : 'width:6em'};
+                    if(!field.match(/count/)) dijitArgs.style = 'width:9em';
                     if(entry.isnew() && field == 'phys_item_count') {
                         // by default, attempt to pay for all non-canceled and as-of-yet-un-invoiced items
                         var count = Number(li.order_summary().item_count() || 0) - 
index 8c078fc..e4dc715 100644 (file)
@@ -61,6 +61,29 @@ function load() {
     var userId = cgi.param('usr');
     var stageUname = cgi.param('stage');
 
+    saveButton.attr("label", localeStrings.SAVE);
+    saveCloneButton.attr("label", localeStrings.SAVE_CLONE);
+    replaceBarcode.attr("label", localeStrings.REPLACE_BARCODE);
+    dojo.byId('uedit-show-required').innerHTML = localeStrings.SHOW_REQUIRED;
+    dojo.byId('uedit-show-suggested').innerHTML = localeStrings.SHOW_SUGGESTED;
+    dojo.byId('uedit-show-all').innerHTML = localeStrings.SHOW_ALL;
+    dojo.byId('uedit-dupe-barcode-warning').innerHTML = localeStrings.BARCODE_IN_USE;
+    allCards.attr("label", localeStrings.SEE_ALL);
+    dojo.byId('uedit-dupe-username-warning').innerHTML = localeStrings.DUPE_USERNAME;
+    generatePassword.attr("label", localeStrings.RESET_PASSWORD);
+    dojo.byId('verifyPassword').innerHTML = localeStrings.VERIFY_PASSWORD;
+    dojo.byId('parentGuardian').innerHTML = localeStrings.PARENT_OR_GUARDIAN;
+    dojo.byId('userSettings').innerHTML = localeStrings.USER_SETTINGS;
+    dojo.byId('statCats').innerHTML = localeStrings.STAT_CATS;
+
+    dojo.query("td[name='addressHeader']").forEach( function(item) { item.innerHTML = localeStrings.ADDRESS_HEADER; });
+    dojo.query("span[name='mailingAddress']").forEach( function(item) { item.innerHTML = localeStrings.ADDRESS_MAILING; });
+    dojo.query("span[name='billingAddress']").forEach( function(item) { item.innerHTML = localeStrings.ADDRESS_BILLING; });
+    dojo.query("span[name='addressPending']").forEach( function(item) { item.innerHTML = localeStrings.ADDRESS_PENDING; });
+    dojo.query("button[name='approve-button']").forEach( function(item) { item.innerHTML = localeStrings.ADDRESS_APPROVE; });
+    dojo.query("span[name='address-already-owned']").forEach( function(item) { item.innerHTML = localeStrings.ADDRESS_OWNED; });
+    dojo.query("button[name='addressNew']").forEach( function(item) { item.innerHTML = localeStrings.ADDRESS_NEW; });
+
     if(xulG) {
            if(xulG.ses) openils.User.authtoken = xulG.ses;
            if(typeof xulG.clone != 'undefined') cloneUser = xulG.clone;
@@ -187,6 +210,8 @@ function load() {
 
     checkGrpAppPerm(); // to do the initial load
     loadStaticFields();
+
+
     if(patron.isnew() && patron.addresses().length == 0) 
         uEditNewAddr(null, uEditAddrVirtId, true);
     else loadAllAddrs();
@@ -601,7 +626,7 @@ function loadStatCats() {
         var span = valtd.appendChild(document.createElement('span'));
         var store = new dojo.data.ItemFileReadStore(
                 {data:fieldmapper.actsc.toStoreData(stat.entries())});
-        var comboBox = new dijit.form.ComboBox({store:store,scrollOnFocus:false}, span);
+        var comboBox = new dijit.form.ComboBox({store:store,scrollOnFocus:false,fetchProperties:{sort:[{attribute: 'value'}]}}, span);
         comboBox.labelAttr = 'value';
         comboBox.searchAttr = 'value';
 
@@ -627,13 +652,16 @@ function loadSurveys() {
     // draw surveys
     for(var idx in surveys) {
         var survey = surveys[idx];
+        var required = openils.Util.isTrue(survey.required());
         var srow = surveyTemplate.cloneNode(true);
+        if(required) srow.setAttribute('required','required');
         tbody.appendChild(srow);
         getByName(srow, 'name').innerHTML = survey.name();
 
         for(var q in survey.questions()) {
             var quest = survey.questions()[q];
             var qrow = surveyQuestionTemplate.cloneNode(true);
+            if(required) qrow.setAttribute('required','required');
             tbody.appendChild(qrow);
             getByName(qrow, 'question').innerHTML = quest.question();
 
@@ -641,7 +669,7 @@ function loadSurveys() {
             var store = new dojo.data.ItemFileReadStore(
                 {data:fieldmapper.asva.toStoreData(quest.answers())});
             var select = new dijit.form.FilteringSelect({store:store,scrollOnFocus:false}, span);
-            if (! openils.Util.isTrue(survey.required())) {
+            if (! required ) {
                 select.isValid = function() { return true; };
             }
             select.labelAttr = 'answer';
@@ -1515,7 +1543,7 @@ function uEditNewAddr(evt, id, mkLinks) {
                 }
 
             } else if(row.getAttribute('name') == 'uedit-addr-divider') {
-                // link up the billing/mailing address and give the inputs IDs so we can acces the later
+                // link up the billing/mailing address and give the inputs IDs so we can access the later
                 
                 // billing address
                 var ba = getByName(row, 'billing_address');
index fb7e927..9cf9140 100644 (file)
 <!ENTITY staff.serial.manage_items.mode "Mode:">
 <!ENTITY staff.serial.manage_items.bind.label "Bind">
 <!ENTITY staff.serial.manage_items.receive.label "Receive">
+<!ENTITY staff.serial.manage_items.advanced_receive.label "Adv. Receive">
 <!ENTITY staff.serial.manage_items.show_all.label "Show All">
 <!ENTITY staff.serial.manage_items.receive_move.label "Receive/Move Selected &#8595;">
 <!ENTITY staff.serial.manage_items.set_current_unit.label "Set Current Unit">
 <!ENTITY staff.serial.manage_items.auto_per_item.label "Auto per Item">
 <!ENTITY staff.serial.manage_items.new_unit.label "New Unit">
+<!ENTITY staff.serial.manage_items.no_unit.label "No Unit">
 <!ENTITY staff.serial.manage_items.recent.label "Recent">
 <!ENTITY staff.serial.manage_items.other_unit.label "Other...">
+<!ENTITY staff.serial.manage_items.context.label "Context:">
 
 <!ENTITY staff.serial.batch_receive "Batch Receive">
 <!ENTITY staff.serial.batch_receive.bib_search_term.label "Enter an identifier for a bibliographic record:">
 <!ENTITY staff.circ.checkin_overlay.actions.label "Actions for Selected Items">
 <!ENTITY staff.circ.checkin_overlay.actions.accesskey "S">
 <!ENTITY staff.circ.checkin_overlay.checkin_export.label "Export">
+<!ENTITY staff.circ.checkin_overlay.printer_prompt.label "Printer Prompt">
 <!ENTITY staff.circ.checkin_overlay.trim_list.label "Trim List (20 rows)">
 <!ENTITY staff.circ.checkin_overlay.async_checkin.label "Fast Entry (Asynchronous)">
 <!ENTITY staff.circ.checkin_overlay.strict_barcode.label "Strict Barcode">
 <!ENTITY staff.cat.copy_browser.actions.cmd_create_brt.accesskey "K">
 <!ENTITY staff.cat.copy_browser.actions.sel_patron.label "Show Last Few Circulations">
 <!ENTITY staff.cat.copy_browser.actions.sel_patron.accesskey "L">
-<!ENTITY staff.cat.copy_browser.actions.cmd_edit_items.label "Edit Item Attributes / Call Numbers / Replace Barcodes">
+<!ENTITY staff.cat.copy_browser.actions.cmd_edit_items.label "Edit Items">
 <!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.holdings_maintenance.sel_patron.label "Show Last Few Circulations">
 <!ENTITY staff.cat.copy_browser.holdings_maintenance.sel_patron.accesskey "L">
-<!ENTITY staff.cat.copy_browser.holdings_maintenance.cmd_edit_items.label "Edit Item Attributes / Call Numbers / Replace Barcodes">
+<!ENTITY staff.cat.copy_browser.holdings_maintenance.cmd_edit_items.label "Edit Items">
 <!ENTITY staff.cat.copy_browser.holdings_maintenance.cmd_edit_items.accesskey "E">
 <!ENTITY staff.cat.copy_browser.holdings_maintenance.cmd_transfer_items.label "Transfer Items to Previously Marked Volume">
 <!ENTITY staff.cat.copy_browser.holdings_maintenance.cmd_transfer_items.accesskey "T">
index 45a2e19..1197a74 100644 (file)
@@ -251,3 +251,8 @@ tr[name="myopac_invalid_addr_row"] td {
     background:#e0e0e0;
     color:red;
 }
+
+/* Table of contents layout */
+td.toc_label { text-align: right; }
+td.toc_title { text-align: left; padding-left: 1em; padding-right: 2em; }
+td.toc_page { text-align: right; }
index a6311b7..897b1ea 100644 (file)
@@ -295,7 +295,6 @@ function advBuildSearchBlob() {
                                break;
                }
                if(string) {
-                       string = string.replace(/'/g,' ');
                        string = string.replace(/\\/g,' ');
             string = string.replace(/^\s*/,'');
             string = string.replace(/\s*$/,'');
index 232e37d..94b8c8b 100644 (file)
@@ -105,7 +105,18 @@ function _cnBrowseDraw( list ) {
                var author_td           = $n(currentTd, 'cn_browse_author');
                var pic_td                      = $n(currentTd, 'cn_browse_pic');
 
+               if (parseInt(cn.prefix().id()) > -1) {
+            cn_td.appendChild(text(cn.prefix().label()));
+            cn_td.appendChild(text(' '));
+        }
+
                cn_td.appendChild(text(cn.label()));
+
+               if (parseInt(cn.suffix().id()) > -1) {
+            cn_td.appendChild(text(' '));
+            cn_td.appendChild(text(cn.suffix().label()));
+        }
+
                lib_td.appendChild(text(findOrgUnit(cn.owning_lib()).name()));
                cnBrowseDrawTitle(mods, title_td, author_td, pic_td);
 
index 50361b6..c4c623a 100644 (file)
@@ -43,7 +43,7 @@ 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') {
+    if (callnumber == null) {
         addCSSClass(print,'hide_me');
     }
 
@@ -123,11 +123,12 @@ function cpdStylePopupWindow(div) {
 
 
 /* builds a friendly print window for this CNs data */
-function cpdBuildPrintPane(contextRow, record, callnumber, orgid, depth) {
+function cpdBuildPrintPane(contextRow, record, cn, orgid, depth) {
 
        var div = cpdBuildPrintWindow( record, orgid);
 
-       $n(div, 'cn').appendChild(text(callnumber));
+    var whole_cn_text = (cn[0] ? cn[0] + ' ' : '') + cn[1] + (cn[2] ? ' ' + cn[2] : '');
+       $n(div, 'cn').appendChild(text(whole_cn_text));
 
        unHideMe($n(div, 'copy_header'));
 
@@ -250,7 +251,19 @@ function cpdDrawCopy(r) {
         return;
     }
 
-       var b = $n(row, 'barcode').appendChild(text(copy.barcode()));
+    // Make barcode more useful for staff client usage
+    if(isXUL()) {
+        var my_a = document.createElement('a');
+        my_a.appendChild(text(copy.barcode()));
+        my_a.setAttribute("href","javascript:void(0);");
+        my_a.onclick = function() {
+            xulG.new_tab(xulG.urls.XUL_COPY_STATUS, {}, {'from_item_details_new': true, 'barcodes': [copy.barcode()]});
+               };
+        $n(row, 'barcode').appendChild(my_a);
+    }
+    else {
+       $n(row, 'barcode').appendChild(text(copy.barcode()));
+    }
 
     /* show the peer type*/
     if (pt) {
index 8f136b1..12fbaf1 100644 (file)
@@ -1633,7 +1633,7 @@ function myopacProcessHolds(action, thawDate) {
         switch(action) { 
 
             case 'cancel':
-                req = new Request(CANCEL_HOLD, G.user.session, hold.id());
+                   req = new Request(CANCEL_HOLD, G.user.session, hold.id(), /* Patron via OPAC */ 6);
                 break;
     
             case 'thaw':
index 4b2984d..fd7b94a 100644 (file)
@@ -810,7 +810,7 @@ function rdetailShowExtra(type, args) {
 function rdetailVolumeDetails(args) {
        var row = $(args.rowid);
        var tbody = row.parentNode;
-       cpdBuild( tbody, row, record, args.cn, args.org, args.depth, args.copy_location );
+       cpdBuild( tbody, row, record, [args.cn_prefix, args.cn, args.cn_suffix], args.org, args.depth, args.copy_location );
        return;
 }
 
@@ -819,7 +819,7 @@ function rdetailBuildCNList() {
        var select = $('cn_browse_selector');
        var index = 0;
        var arr = [];
-       for( var cn in callnumberCache ) arr.push( cn );
+       for( var cn_json in callnumberCache ) arr.push( cn_json );
        arr.sort();
 
        if( arr.length == 0 ) {
@@ -828,8 +828,10 @@ function rdetailBuildCNList() {
        }
 
        for( var i = 0; i < arr.length; i++ ) {
-               var cn = arr[i];
-               var opt = new Option(cn);
+               var cn_json = arr[i];
+        var cn = JSON2js(cn_json);
+        var whole_cn_text = (cn[0] ? cn[0] + ' ' : '') + cn[1] + (cn[2] ? ' ' + cn[2] : '');
+               var opt = new Option(whole_cn_text,cn_json);
                select.options[index++] = opt;
        }
        select.onchange = rdetailGatherCN;
@@ -837,7 +839,7 @@ function rdetailBuildCNList() {
 
 function rdetailGatherCN() {
        var cn = getSelectorVal($('cn_browse_selector'));
-       rdetailShowCNBrowse( cn, getLocation(), getDepth(), true );
+       rdetailShowCNBrowse( JSON2js(cn), getLocation(), getDepth(), true );
        setSelector( $('cn_browse_selector'), cn );
 }
 
@@ -852,7 +854,7 @@ function rdetailShowCNBrowse( cn, loc, depth, fromOnclick ) {
 
        unHideMe($('rdetail_cn_browse_select_div'));
        rdetailBuildCNList();
-       setSelector( $('cn_browse_selector'), cn );
+       setSelector( $('cn_browse_selector'), js2JSON(cn) );
        hideMe($('rdetail_copy_info_div'));
        hideMe($('rdetail_reviews_div'));
        hideMe($('rdetail_summary_div'));
@@ -926,7 +928,6 @@ function rdetailBuildInfoRows() {
        var method = FETCH_COPY_COUNTS_SUMMARY;
        if (rdetailShowCopyLocation)
                method = FETCH_COPY_LOCATION_COUNTS_SUMMARY;
-
        if( rdetailShowLocal ) 
                req = new Request(method, record.doc_id(), getLocation(), getDepth())
        else
@@ -1036,7 +1037,7 @@ function _rdetailBuildInfoRows(r) {
        for( var i = 0; i < summary.length; i++ ) {
 
                var arr = summary[i];
-               globalCNCache[arr[1]] = 1;
+               globalCNCache[js2JSON([arr[1],arr[2],arr[3]])] = 1; // prefix, label, suffix.  FIXME - Am I used anywhere?
                var thisOrg = findOrgUnit(arr[0]);
                var rowNode = $("cp_info_" + thisOrg.id());
                if(!rowNode) continue;
@@ -1070,11 +1071,11 @@ function _rdetailBuildInfoRows(r) {
                var cpc_temp = rowNode.removeChild(
                                findNodeByName(rowNode, config.names.rdetail.cp_count_cell));
 
-               var statuses = arr[2];
+               var statuses = arr[4];
                var cl = '';
                if (rdetailShowCopyLocation) {
-                       cl = arr[2];
-                       statuses = arr[3];
+                       cl = arr[4];
+                       statuses = arr[5];
                }
 
 
@@ -1086,7 +1087,7 @@ function _rdetailBuildInfoRows(r) {
                        isLocal = true; 
                        if(!localCNFound) {
                                localCNFound = true;
-                               defaultCN = arr[1];
+                               defaultCN = [arr[1],arr[2],arr[3]]; // prefix, label, suffix
                        }
                }
 
@@ -1094,9 +1095,9 @@ function _rdetailBuildInfoRows(r) {
                unHideMe(rowNode);
 
                rdetailSetPath( thisOrg, isLocal );
-               rdetailBuildBrowseInfo( rowNode, arr[1], isLocal, thisOrg, cl );
+               rdetailBuildBrowseInfo( rowNode, [arr[1],arr[2],arr[3]], isLocal, thisOrg, cl );
 
-               if( i == summary.length - 1 && !defaultCN) defaultCN = arr[1];
+               if( i == summary.length - 1 && !defaultCN) defaultCN = [arr[1],arr[2],arr[3]]; // prefix, label, suffix
        }
 
        if(!found) unHideMe(G.ui.rdetail.cp_info_none);
@@ -1104,16 +1105,19 @@ function _rdetailBuildInfoRows(r) {
 
 function rdetailBuildBrowseInfo(row, cn, local, orgNode, cl) {
 
+    var whole_cn_json = js2JSON(cn);
+    var whole_cn_text = (cn[0] ? cn[0] + ' ' : '') + cn[1] + (cn[2] ? ' ' + cn[2] : '');
+
        if(local) {
-               var cache = callnumberCache[cn];
+               var cache = callnumberCache[whole_cn_json];
                if( cache ) cache.count++;
-               else callnumberCache[cn] = { count : 1 };
+               else callnumberCache[whole_cn_json] = { count : 1 };
        }
 
        var depth = getDepth();
        if( !local ) depth = findOrgDepth(globalOrgTree);
 
-       $n(row, 'rdetail_callnumber_cell').appendChild(text(cn));
+       $n(row, 'rdetail_callnumber_cell').appendChild(text(whole_cn_text));
 
        if (rdetailShowCopyLocation) {
                var cl_cell = $n(row, 'rdetail_copylocation_cell');
@@ -1121,12 +1125,12 @@ function rdetailBuildBrowseInfo(row, cn, local, orgNode, cl) {
                unHideMe(cl_cell);
        }
 
-       _debug('setting action clicks for cn ' + cn);
+       _debug('setting action clicks for cn ' + whole_cn_text);
 
        var dHref = 'javascript:rdetailVolumeDetails('+
-                       '{copy_location : "'+cl.replace(/\"/g, '\\"')+'", rowid : "'+row.id+'", cn :"'+cn.replace(/\"/g, '\\"')+'", depth:"'+depth+'", org:"'+orgNode.id()+'", local: '+local+'});';
+                       '{copy_location : "'+cl.replace(/\"/g, '\\"')+'", rowid : "'+row.id+'", cn_prefix :"'+cn[0].replace(/\"/g, '\\"')+'",cn :"'+cn[1].replace(/\"/g, '\\"')+'",cn_suffix :"'+cn[2].replace(/\"/g, '\\"')+'", depth:"'+depth+'", org:"'+orgNode.id()+'", local: '+local+'});';
 
-       var bHref = 'javascript:rdetailShowCNBrowse("' + cn.replace(/\"/g, '\\"') + '", '+orgNode.id()+', "'+depth+'");'; 
+       var bHref = 'javascript:rdetailShowCNBrowse("'+cn[1].replace(/\"/g, '\\"') + '", '+orgNode.id()+', "'+depth+'");'; 
 
        unHideMe( $n(row, 'details') )
                $n(row, 'details').setAttribute('href', dHref);
index fd7d5ad..ab8edbb 100644 (file)
      }, lang, bidi;
 </script>
 
-<script language='javascript' src='/js/dojo/dojo/dojo.js'></script>
-<script language='javascript' type='text/javascript' src='<!--#echo var="OILS_JS_BASE"-->/JSON_v1.js'></script>
-<script language='javascript' type="text/javascript" src='/js/dojo/opensrf/opensrf.js'></script>
-<script language='javascript' type="text/javascript" src='/js/dojo/dojo/openils_dojo.js'></script>
-<script language='javascript' type="text/javascript" src='/js/dojo/fieldmapper/AutoIDL.js'></script>
+<script language='javascript' src='/js/dojo/dojo/dojo.js?<!--#include virtual="/eg_cache_hash"-->'></script>
+<script language='javascript' type='text/javascript' src='<!--#echo var="OILS_JS_BASE"-->/JSON_v1.js?<!--#include virtual="/eg_cache_hash"-->'></script>
+<script language='javascript' type="text/javascript" src='/js/dojo/opensrf/opensrf.js?<!--#include virtual="/eg_cache_hash"-->'></script>
+<script language='javascript' type="text/javascript" src='/js/dojo/dojo/openils_dojo.js?<!--#include virtual="/eg_cache_hash"-->'></script>
+<script language='javascript' type="text/javascript" src='/js/dojo/fieldmapper/AutoIDL.js?<!--#include virtual="/eg_cache_hash"-->'></script>
 
-<script language='javascript' type="text/javascript" src='<!--#echo var="OILS_JS_BASE"-->/<!--#echo var="locale"-->/OrgTree.js'></script>
-<script language='javascript' type="text/javascript" src='<!--#echo var="OILS_JS_BASE"-->/<!--#echo var="locale"-->/FacetDefs.js'></script>
-<script language='javascript' type="text/javascript" src='<!--#echo var="OILS_JS_BASE"-->/OrgLasso.js'></script>
+<script language='javascript' type="text/javascript" src='<!--#echo var="OILS_JS_BASE"-->/<!--#echo var="locale"-->/OrgTree.js?<!--#include virtual="/eg_cache_hash"-->'></script>
+<script language='javascript' type="text/javascript" src='<!--#echo var="OILS_JS_BASE"-->/<!--#echo var="locale"-->/FacetDefs.js?<!--#include virtual="/eg_cache_hash"-->'></script>
+<script language='javascript' type="text/javascript" src='<!--#echo var="OILS_JS_BASE"-->/OrgLasso.js?<!--#include virtual="/eg_cache_hash"-->'></script>
 
 <!--#if expr="$OILS_OPAC_COMBINED_JS"-->
-<script language='javascript' type='text/javascript' src='<!--#echo var="OILS_OPAC_JS_HOST"-->/skin/default/js/combined.js'></script>
+<script language='javascript' type='text/javascript' src='<!--#echo var="OILS_OPAC_JS_HOST"-->/skin/default/js/combined.js?<!--#include virtual="/eg_cache_hash"-->'></script>
 <!--#else --> 
 <!-- 
     When combined JS is enabled in the Apache config, the block 
index 526577c..05920f5 100644 (file)
@@ -90,7 +90,7 @@
             </tbody>
             <thead>
                 <tr>
-                    <th colspan='4'/>
+                    <th colspan='3'/>
                     <th class='acq-invoice-center-col' class='acq-invoice-billed-col'>Total</th>
                     <th class='acq-invoice-paid-col'>Total</th>
                     <th class='acq-invoice-center-col' class='acq-invoice-balance-col'>Balance</th>
@@ -98,7 +98,7 @@
             </thead>
             <tbody>
                 <tr>
-                    <td colspan='4' style='text-align:right;'>
+                    <td colspan='3' style='text-align:right;'>
                         <button jsId='invoiceSaveButton' class='hide-complete'
                             dojoType='dijit.form.Button' onclick='saveChanges();'>Save</button>
                         <button jsId='invoiceProrateButton' class='hide-complete'
                                 dojoType='dijit.form.Button' onclick='saveChanges(false, false, true);'>Reopen Invoice</button>
                         </span>
                     </td>
-                    <td class='acq-invoice-center-col'><div jsId='totalInvoicedBox' dojoType='dijit.form.CurrencyTextBox' style='width:6em;'/></td>
-                    <td class='acq-invoice-paid-col'><div jsId='totalPaidBox' dojoType='dijit.form.CurrencyTextBox' style='width:6em;'/></td>
-                    <td class='acq-invoice-center-col'><div jsId='balanceOwedBox' dojoType='dijit.form.CurrencyTextBox' style='width:6em;'/></td>
+                    <td class='acq-invoice-center-col'><div jsId='totalInvoicedBox' dojoType='dijit.form.CurrencyTextBox' style='width:9em;'/></td>
+                    <td class='acq-invoice-paid-col'><div jsId='totalPaidBox' dojoType='dijit.form.CurrencyTextBox' style='width:9em;'/></td>
+                    <td class='acq-invoice-center-col'><div jsId='balanceOwedBox' dojoType='dijit.form.CurrencyTextBox' style='width:9em;'/></td>
                 </tr>
             </tbody>
         </table>
index aa1dad5..a821020 100644 (file)
@@ -15,8 +15,8 @@
 </div>
 
 <div id='uedit-save-div'>
-    <button dojoType='dijit.form.Button' jsId='saveButton' onClick='uEditSave' scrollOnFocus='false'>Save</button>
-    <button dojoType='dijit.form.Button' jsId='saveCloneButton' onClick='uEditSaveClone' scrollOnFocus='false'>Save &amp; Clone</button>
+    <button dojoType='dijit.form.Button' jsId='saveButton' onClick='uEditSave' scrollOnFocus='false'></button>
+    <button dojoType='dijit.form.Button' jsId='saveCloneButton' onClick='uEditSaveClone' scrollOnFocus='false'></button>
     <div id='require-toggle'>
         <a href='javascript:uEditToggleRequired(1);' id='uedit-show-required'>Show Only Required Fields</a><br id='uedit-show-required-br'/>
         <a href='javascript:uEditToggleRequired(2);' id='uedit-show-suggested'>Show Suggested Fields</a><br id='uedit-show-suggested-br'/>
index 711e56f..5528252 100644 (file)
@@ -2,30 +2,28 @@
     <tr fmclass='ac' fmfield='barcode' required='required'>
         <td/><td/><td/>
         <td>
-            <button dojoType='dijit.form.Button' jsId='replaceBarcode' scrollOnFocus='false'>Replace Barcode</button>
+            <button dojoType='dijit.form.Button' jsId='replaceBarcode' scrollOnFocus='false'></button>
             <span id='uedit-dupe-barcode-warning' style='color:red; font-weight:bold' class='hidden'>
-                Barcode is already in use
             </span>
         </td>
         <td id='uedit-all-barcodes' class='hidden'>
-            <button dojoType='dijit.form.Button' jsId='allCards' scrollOnFocus='false'>See All</button>
+            <button dojoType='dijit.form.Button' jsId='allCards' scrollOnFocus='false'></button>
         </td>
     </tr>
     <tr fmclass='au' fmfield='usrname' required='required'>
         <td/><td/><td/>
         <td>
             <span id='uedit-dupe-username-warning' style='color:red; font-weight:bold' class='hidden'>
-                Username is already in use
             </span>
         </td>
     </tr>
     <tr fmclass='au' fmfield='passwd' required='required'>
         <td/><td/><td/>
         <td>
-            <button dojoType='dijit.form.Button' jsId='generatePassword' scrollOnFocus='false' tabIndex='-1'>Reset Password</button>
+            <button dojoType='dijit.form.Button' jsId='generatePassword' scrollOnFocus='false' tabIndex='-1'></button>
         </td>
     </tr>
-    <tr fmclass='au' fmfield='passwd2' required='required'><td/><td>Verify Password</td><td/></tr>
+    <tr fmclass='au' fmfield='passwd2' required='required'><td/><td id='verifyPassword'></td><td/></tr>
     <tr fmclass='au' fmfield='first_given_name' required='required'/>
     <tr fmclass='au' fmfield='second_given_name'/>
     <tr fmclass='au' fmfield='family_name' required='required'/>
@@ -35,7 +33,7 @@
     <tr fmclass='au' fmfield='juvenile'/>
     <tr fmclass='au' fmfield='ident_type' required='required'/>
     <tr fmclass='au' fmfield='ident_value'/>
-    <tr fmclass='au' fmfield='ident_value2'><td/><td>Parent/Guardian</td></tr>
+    <tr fmclass='au' fmfield='ident_value2'><td/><td id='parentGuardian'></td></tr>
     <tr fmclass='au' fmfield='email'/>
     <tr fmclass='au' fmfield='day_phone'/>
     <tr fmclass='au' fmfield='evening_phone'/>
@@ -51,7 +49,7 @@
     <tr fmclass='au' fmfield='claims_never_checked_out_count' wclass='dijit.form.NumberSpinner' wconstraints="{min:0,places:0}" wvalue='0'/>
     <tr fmclass='au' fmfield='alert_message' wclass='dijit.form.Textarea' wstyle='height:5em'/>
 
-    <tr class='divider hidden' id='uedit-settings-divider'><td colspan='0'>User Settings</td></tr>
+    <tr class='divider hidden' id='uedit-settings-divider'><td colspan='0' id='userSettings'></td></tr>
     <tr class='hidden' id='uedit-user-setting-template'>
         <td/>
         <td><span name='label'></span></td>
 
     <!-- Address -->
     <tr name='uedit-addr-divider' class='divider' type='addr-template' required='show'>
-        <td colspan='2'>Address</td>
+        <td colspan='2' name='addressHeader'></td>
         <td>
-            <span>Mailing</span><input type='radio' name='mailing_address'>
-            <span>Billing</span><input type='radio' name='billing_address'>
+            <span name='mailingAddress'></span><input type='radio' name='mailing_address'>
+            <span name='billingAddress'></span><input type='radio' name='billing_address'>
             <button dojoType='dijit.form.Button' scrollOnFocus='false' name='delete-button' class='uedit-addr-del-button'>X</button>
         </td>
     </tr>
 
     <tr name='uedit-addr-pending-row' type='addr-template' class='pending-addr-row hidden'>
         <td colspan='3'>
-            <span style='padding-right:10px;'>This is a pending address: </span>
-            <button dojoType='dijit.form.Button' scrollOnFocus='false'  name='approve-button'>Approve Address</button>
+            <span style='padding-right:10px;' name='addressPending'></span>
+            <button dojoType='dijit.form.Button' scrollOnFocus='false'  name='approve-button'></button>
             <div name='replaced-addr-div'>
                 <div name='replaced-addr'></div>
             </div>
@@ -80,7 +78,7 @@
 
     <tr name='uedit-addr-owner-row' type='addr-template' class='pending-addr-row hidden'>
         <td colspan='3'>
-            <span style='padding-right:10px;'>This address is owned by another user: </span>
+            <span style='padding-right:10px;' name='address-already-owned'></span>
             <a href='javascript:void(0);'  name='addr-owner'></a>
         </td>
     </tr>
 
     <tr id='new-addr-row' class='newaddr-row' required='show'>
         <td colspan='0' style='text-align:center;'>
-            <button dojoType='dijit.form.Button' onClick='uEditNewAddr' scrollOnFocus='false'>New Address</button>
+            <button dojoType='dijit.form.Button' onClick='uEditNewAddr' scrollOnFocus='false' name='addressNew'></button>
         </td>
     </tr>
 
     <!-- stat cats -->
-    <tr class='divider' id='stat-cat-divider' required='suggested'><td colspan='0'>Statistical Categories</td></tr>
+    <tr class='divider' id='stat-cat-divider' required='suggested'><td colspan='0' id='statCats'></td></tr>
     <tr id='stat-cat-row-template' required='suggested'><td class='uedit-help'/><td name='name'/><td name='widget'/></tr>
 
     <!-- surveys -->
index 26b0977..bd19e7e 100644 (file)
@@ -9,7 +9,7 @@
     <table  jsId="cmGrid"
             style="height: 600px;"
             dojoType="openils.widget.AutoGrid"
-            fieldOrder="['id', 'active', 'grp', 'org_unit', 'copy_circ_lib', 'copy_owning_lib', 'user_home_ou', 'is_renewal', 'juvenile_flag', 'circ_modifier', 'marc_type', 'marc_form', 'marc_vr_format', 'ref_flag', 'usr_age_lower_bound', 'usr_age_upper_bound', 'circulate', 'duration_rule', 'renewals', 'hard_due_date', 'recurring_fine_rule', 'grace_period', 'max_fine_rule', 'available_copy_hold_ratio', 'total_copy_hold_ratio', 'script_test']"
+            fieldOrder="['id', 'active', 'grp', 'org_unit', 'copy_circ_lib', 'copy_owning_lib', 'user_home_ou', 'is_renewal', 'juvenile_flag', 'circ_modifier', 'marc_type', 'marc_form', 'marc_bib_level', 'marc_vr_format', 'ref_flag', 'usr_age_lower_bound', 'usr_age_upper_bound', 'circulate', 'duration_rule', 'renewals', 'hard_due_date', 'recurring_fine_rule', 'grace_period', 'max_fine_rule', 'available_copy_hold_ratio', 'total_copy_hold_ratio', 'script_test']"
             defaultCellWidth='"auto"'
             query="{id: '*'}"
             fmClass='ccmm'
index 55ca6b5..9315214 100644 (file)
@@ -8,7 +8,7 @@
     <table  jsId="hmGrid" 
             autoHeight='true'
             dojoType="openils.widget.AutoGrid" 
-            fieldOrder="['id', 'strict_ou_match', 'user_home_ou', 'request_ou', 'pickup_ou', 'item_owning_ou', 'item_circ_ou', 'requestor_grp', 'circ_modifier']"
+            fieldOrder="['id', 'strict_ou_match', 'user_home_ou', 'request_ou', 'pickup_ou', 'item_owning_ou', 'item_circ_ou', 'requestor_grp', 'circ_modifier', 'marc_type', 'marc_form', 'marc_bib_level', 'marc_vr_format']"
             defaultCellWidth='"auto"'
             query="{id: '*'}" 
             fmClass='chmm' 
index 396de5a..d210ef2 100644 (file)
@@ -9,7 +9,7 @@ export STAFF_CLIENT_STAMP_ID = $$(/bin/cat build/STAMP_ID)
 
 # from http://closure-compiler.googlecode.com/files/compiler-latest.zip  FIXME: Autotools this?
 export CLOSURE_COMPILER_JAR = ~/closure-compiler/compiler.jar
-XULRUNNER_VERSION=1.9.2.16
+XULRUNNER_VERSION=1.9.2.17
 XULRUNNER_WINFILE=xulrunner-$(XULRUNNER_VERSION).en-US.win32.zip
 XULRUNNER_LINUXFILE=xulrunner-$(XULRUNNER_VERSION).en-US.linux-i686.tar.bz2
 XULRUNNER_URL=http://releases.mozilla.org/pub/mozilla.org/xulrunner/releases/$(XULRUNNER_VERSION)/runtimes/
@@ -19,13 +19,15 @@ CHROME_LOCALES = $$(ls -1 chrome/locale)
 SKIN_CSS = $$(ls -1 server/skin/*css | sed -e "s/.css/_custom.css/")
 UPDATESDIR=@localstatedir@/updates
 
-SVN=svn # Because some people might need to override this to 'git svn' or something
+GIT_BRANCH=$$(git rev-parse --abbrev-ref HEAD || echo master)
+GIT_TAG=$$(git rev-parse --short HEAD) # For auto-tagging builds
 
 export NSIS_EXTRAOPTS
 export NSIS_WICON=$$(if [ -f client/evergreen.ico ]; then echo '-DWICON'; fi)
 export NSIS_AUTOUPDATE=$$([ -f client/defaults/preferences/autoupdate.js ] && echo '-DAUTOUPDATE')
 export NSIS_DEV=$$([ -f client/defaults/preferences/developers.js ] && echo '-DDEVELOPER')
 export NSIS_PERMACHINE=$$([ -f client/defaults/preferences/aa_per_machine.js ] && echo '-DPERMACHINE')
+export NSIS_EXTRAS=$$([ -f extras.nsi ] && echo '-DEXTRAS')
 # Url taken from http://nsis.sourceforge.net/AccessControl_plug-in
 NSIS_ACCESSCONTROL=http://nsis.sourceforge.net/mediawiki/images/4/4a/AccessControl.zip
 
@@ -112,18 +114,18 @@ localize_manifest:
 
 # The default "automatic" BUILD ID is acceptable.
 
-# The version from the README usually conforms to that documentation, unless it is trunk.
-# If we are in trunk, we probably have svn kicking around, ask it for the revision and build an appropriate version string.
+# The version from the README usually conforms to that documentation, unless it is master.
+# If we are in master, we probably have git kicking around, ask it for the revision and build an appropriate version string.
 
 # Neither really applies to the STAMP, though.
 # The method below gives the same format STAMPS as previous instructions provided. If README has version 1.2.3.4 then STAMP_ID will become rel_1_2_3_4.
-# Trunk VERSION will end up with 0trunk.release, trunk STAMP ID will be 0trunk_release.
+# Master VERSION will end up with 0branch.release, master STAMP ID will be 0branch_release.
 stamp:
        @/bin/date +"%Y%m%d.%H%M%S" > build/BUILD_ID
        @if [ -n "${STAFF_CLIENT_BUILD_ID}" ]; then ( echo "Stamping with Build ID: ${STAFF_CLIENT_BUILD_ID}" ; echo ${STAFF_CLIENT_BUILD_ID} > build/BUILD_ID ) ; fi
        @if [ -z "${STAFF_CLIENT_BUILD_ID}" ]; then ( echo "No Build ID for versioning" ; echo "none" > build/BUILD_ID ) ; fi
        @sed -n -e '1 s/^.* \([^ ]*\)$$/\1/p' @top_srcdir@/README > build/VERSION
-       @if [ "${STAFF_CLIENT_VERSION}" == "trunk" ]; then echo "0trunk.$$(${SVN} info | sed -n -e 's/Last Changed Rev: \([0-9][0-9]*\)/\1/p')" > build/VERSION; fi 
+       @if [ "${STAFF_CLIENT_VERSION}" == "master" ]; then echo "0${GIT_BRANCH}.${GIT_TAG}" > build/VERSION; fi 
        @if [ -n "${STAFF_CLIENT_VERSION}" ]; then ( echo "Stamping with Version: ${STAFF_CLIENT_VERSION}" ; echo ${STAFF_CLIENT_VERSION} > build/VERSION ) ; fi
        @if [ -z "${STAFF_CLIENT_VERSION}" ]; then ( echo "No Version" ; echo "none" > build/VERSION ) ; fi
        @sed -e 's/\./_/g' -e 's/^\([0-9_]*\)$$/rel_&/' build/VERSION > build/STAMP_ID
@@ -253,9 +255,23 @@ generic-client: client_app
 # https://developer.mozilla.org/en/XULRunner/Deploying_XULRunner_1.8
 # for their respective platforms in regards to XULRunner deployment
 
+nsis_check:
+       @echo 'Checking for makensis'
+       @type -P makensis > /dev/null || ( echo 'MAKENSIS NOT FOUND: Cannot continue. Do you need to install the NSIS package?' && exit 1 )
+
+unzip_check:
+       @echo 'Checking for unzip'
+       @type -P unzip > /dev/null || ( echo 'UNZIP NOT FOUND: Cannot continue.' && exit 1 )
+
+branding_check:
+       @echo 'Checking for branding'
+       @[ -f xulrunner-stub.exe ] || echo 'xulrunner-stub.exe not found'
+       @[ -f build/evergreen.ico ] || echo 'build/evergreen.ico not found'
+       @if [ ! -f xulrunner-stub.exe -o ! -f build/evergreen.ico ]; then echo 'Branding incomplete. Did you forget to run "make rigbeta" or "make rigrelease"?'; echo 'You will need to "make rebuild" afterwards.'; exit 1; fi
+
 # Note that I decided to use win/lin channels for ease of coding platform specific updates
 
-win-xulrunner: client_app
+win-xulrunner: unzip_check branding_check client_app
        @echo 'Preparing Windows xulrunner'
        @if [ ! -f ${XULRUNNER_WINFILE} ]; then wget ${XULRUNNER_URL}${XULRUNNER_WINFILE}; fi
        @unzip -q ${XULRUNNER_WINFILE} -dclient
@@ -273,10 +289,10 @@ linux-xulrunner: client_app
 
 # Build a windows installer.
 
-win-client: win-xulrunner
+win-client: nsis_check win-xulrunner
        @if [ "${NSIS_AUTOUPDATE}${NSIS_PERMACHINE}" -a ! -d AccessControl ]; then echo 'Fetching AccessControl Plugin'; wget ${NSIS_ACCESSCONTROL} -O AccessControl.zip; unzip AccessControl.zip; fi
        @echo 'Building installer'
-       @makensis -V2 -DPRODUCT_VERSION="${STAFF_CLIENT_VERSION}" ${NSIS_WICON} ${NSIS_AUTOUPDATE} ${NSIS_DEV} ${NSIS_PERMACHINE} ${NSIS_EXTRAOPTS} windowssetup.nsi
+       @makensis -V2 -DPRODUCT_VERSION="${STAFF_CLIENT_VERSION}" ${NSIS_WICON} ${NSIS_AUTOUPDATE} ${NSIS_DEV} ${NSIS_PERMACHINE} ${NSIS_EXTRAS} ${NSIS_EXTRAOPTS} windowssetup.nsi
        @echo 'Done'
 
 # For linux, just build a tar.bz2 archive
index 8250cae..c1c104c 100644 (file)
                         dump('on_oils_persist: <<< ' + target.nodeName + '.id = ' + target.id + '\t' + bk + '\n');
                         for (var j = 0; j < attribute_list.length; j++) {
                             var key = base_key + attribute_list[j];
-                            var value = target.getAttribute( attribute_list[j] );
+                            var value = encodeURI(target.getAttribute( attribute_list[j] ));
                             if ( attribute_list[j] == 'checked' && ['checkbox','toolbarbutton'].indexOf( target.nodeName ) > -1 ) {
                                 value = target.checked;
                                 dump('\t' + value + ' <== .' + attribute_list[j] + '\n');
+                            } else if ( attribute_list[j] == 'value' && ['menulist'].indexOf( target.nodeName ) > -1 ) {
+                                value = target.value;
+                                dump('\t' + value + ' <== .' + attribute_list[j] + '\n');
                             } else if ( attribute_list[j] == 'value' && ['textbox'].indexOf( target.nodeName ) > -1 ) {
                                 value = target.value;
                                 dump('\t' + value + ' <== .' + attribute_list[j] + '\n');
                 for (var j = 0; j < attribute_list.length; j++) {
                     var key = base_key + attribute_list[j];
                     var has_key = prefs.prefHasUserValue(key);
-                    var value = has_key ? prefs.getCharPref(key) : null;
+                    var value = has_key ? decodeURI(prefs.getCharPref(key)) : null;
                     if (value == 'true') { value = true; }
                     if (value == 'false') { value = false; }
                     if (has_key) {
                         } else if ( attribute_list[j] == 'value' && ['textbox'].indexOf( nodes[i].nodeName ) > -1 ) {
                             nodes[i].value = value;
                             dump('\t' + value + ' ==> .' + attribute_list[j] + '\n');
+                        } else if ( attribute_list[j] == 'value' && ['menulist'].indexOf( nodes[i].nodeName ) > -1 ) {
+                            nodes[i].value = value;
+                            dump('\t' + value + ' ==> .' + attribute_list[j] + '\n');       
                         } else if ( attribute_list[j] == 'sizemode' && ['window'].indexOf( nodes[i].nodeName ) > -1 ) {
                             switch(value) {
                                 case window.STATE_MAXIMIZED:
                         }
                     } else {
                         if (node.nodeName == 'textbox') { 
-                            event_types.push('change'); 
+                            event_types.push('change');
+                        } else if (node.nodeName == 'menulist') { 
+                            event_types.push('select');  
                         } else if (node.nodeName == 'window') {
                             event_types.push('resize'); 
                             node = window; // xul window is an element of window.document
index 413a18a..32f6ee9 100644 (file)
@@ -13,6 +13,9 @@
                 try {
                     if (typeof level == 'undefined') { level = 4; }
                     if (level > _dump_level) { return; }
+                    if (typeof _dump_prefix != 'undefined') {
+                        _original_dump(_dump_prefix + ' ');
+                    }
                     switch(level) {
                         case 1: case 'error': _original_dump('error: '); break;
                         case 2: case 'warn': _original_dump('warn: '); break;
index e9d2539..3fb8de2 100644 (file)
@@ -14,6 +14,9 @@
             try {
                 if (typeof level == 'undefined') { level = 4; }
                 if (level > _dump_level) { return; }
+                if (typeof _dump_prefix != 'undefined') {
+                    _original_dump(_dump_prefix + ' ');
+                }
                 switch(level) {
                     case 1: case 'error': _original_dump('error: '); break;
                     case 2: case 'warn': _original_dump('warn: '); break;
index 2cce6c2..2fbe422 100644 (file)
@@ -861,8 +861,15 @@ function add_volumes() {
 
         var title = document.getElementById('offlineStrings').getFormattedString('staff.circ.copy_status.add_volumes.title', [docid]);
 
-        var horizontal_interface = String( g.data.hash.aous['ui.cat.volume_copy_editor.horizontal'] ) == 'true';
-        var url = window.xulG.url_prefix( horizontal_interface ? urls.XUL_VOLUME_COPY_CREATOR_HORIZONTAL : urls.XUL_VOLUME_COPY_CREATOR );
+        var url;
+        var unified_interface = String( g.data.hash.aous['ui.unified_volume_copy_editor'] ) == 'true';
+        if (unified_interface) {
+            var horizontal_interface = String( g.data.hash.aous['ui.cat.volume_copy_editor.horizontal'] ) == 'true';
+            url = window.xulG.url_prefix( horizontal_interface ? urls.XUL_VOLUME_COPY_CREATOR_HORIZONTAL : urls.XUL_VOLUME_COPY_CREATOR );
+        } else {
+            url = window.xulG.url_prefix( urls.XUL_VOLUME_COPY_CREATOR_ORIGINAL );
+        }
+
         var w = xulG.new_tab(
             url,
             { 'tab_name' : title },
index b146f01..bc8c11d 100644 (file)
@@ -579,7 +579,7 @@ main.menu.prototype = {
                     var loc = urls.XUL_BROWSER + '?url=' + window.escape(
                         obj.url_prefix(urls.XUL_HOLD_PULL_LIST)
                     );
-                    obj.command_tab(event, loc, {'tab_name' : offlineStrings.getString('menu.cmd_browse_hold_pull_list.tab')}, { 'show_print_button' : true } );
+                    obj.command_tab(event, loc, {'tab_name' : offlineStrings.getString('menu.cmd_browse_hold_pull_list.tab')} );
                 }
             ],
 
@@ -1928,8 +1928,14 @@ commands:
     },
     'volume_item_creator' : function(params) {
         var obj = this;
-        var horizontal_interface = String( obj.data.hash.aous['ui.cat.volume_copy_editor.horizontal'] ) == 'true';
-        var url = obj.url_prefix( horizontal_interface ? urls.XUL_VOLUME_COPY_CREATOR_HORIZONTAL : urls.XUL_VOLUME_COPY_CREATOR );
+        var url;
+        var unified_interface = String( obj.data.hash.aous['ui.unified_volume_copy_editor'] ) == 'true';
+        if (unified_interface) {
+            var horizontal_interface = String( obj.data.hash.aous['ui.cat.volume_copy_editor.horizontal'] ) == 'true';
+            url = obj.url_prefix( horizontal_interface ? urls.XUL_VOLUME_COPY_CREATOR_HORIZONTAL : urls.XUL_VOLUME_COPY_CREATOR );
+        } else {
+            url = obj.url_prefix( urls.XUL_VOLUME_COPY_CREATOR_ORIGINAL );
+        }
         var w = obj.new_tab(
             url,
             { 'tab_name' : document.getElementById('offlineStrings').getString('staff.cat.create_or_rebarcode_items') },
index c21e34b..0d63ae1 100644 (file)
                     <textbox id="password" type="password"/>
                 </row>
                 <row>
-                    <button label="&common.cancel;" accesskey="&common.cancel.accesskey;" oncommand="window.close()"/>
                     <button label="&staff.main.simple_auth.authorize.label;" accesskey="&staff.main.simple_auth.authorize.accesskey;" oncommand="authorize()"/>
+                    <button label="&common.cancel;" accesskey="&common.cancel.accesskey;" oncommand="window.close()"/>
                 </row>
             </rows>
         </grid>
index aa1608a..4f9cbdd 100644 (file)
@@ -22,6 +22,16 @@ util.print = function (context) {
     }
     this.oils_printer_external_cmd = has_key ? prefs.getCharPref(key) : '';
 
+    try {
+        if (prefs.prefHasUserValue('print.always_print_silent')) {
+            if (! prefs.getBoolPref('print.always_print_silent')) {
+                prefs.clearUserPref('print.always_print_silent');
+            }
+        }
+    } catch(E) {
+        dump('Error in print.js trying to clear print.always_print_silent\n');
+    }
+
     return this;
 };
 
index 266c0d8..2e59cf8 100755 (executable)
@@ -1,5 +1,6 @@
 #!/bin/bash
 find $1 -type d -name CVS -exec rm -rf {} \; 2> /dev/null
 find $1 -type d -name .svn -exec rm -rf {} \; 2> /dev/null
+find $1 -type d -name .git -exec rm -rf {} \; 2> /dev/null
 find $1 -type d -name OPEN_ILS_STAFF_CLIENT -exec rm -rf {} \; 2> /dev/null
 exit 0
diff --git a/Open-ILS/xul/staff_client/server/OpenILS/symbol_overlay.js b/Open-ILS/xul/staff_client/server/OpenILS/symbol_overlay.js
new file mode 100644 (file)
index 0000000..f84a702
--- /dev/null
@@ -0,0 +1,37 @@
+dump('entering symbol/clipboard.js\n');
+
+function $(id) { return document.getElementById(id); }
+
+var el = {};
+
+dojo.addOnLoad(
+    function(){
+        dojo.query('.plain').forEach(function(node,index,arr){
+            node.addEventListener("keypress", function(event) { 
+                if (event.charCode == 115 && event.ctrlKey){
+                        setNod(node);
+                        $('symbol-panel').openPopup(node, 'after_pointer' );
+                    }
+                 }, true);
+        });
+    }
+);
+
+function setNod(elm){
+    el = elm;
+}
+
+function ret(ins, e){
+    if (e.button == 0){
+        $('symbol-panel').hidePopup();
+        n = el;
+        
+        if (n.getAttribute('readonly')=='true') return;
+        
+        var v = n.value;
+        var start = n.selectionStart;
+        var end = n.selectionEnd;
+        n.value = v.substring(0, start) + ins + v.substring(end, v.length);
+        n.setSelectionRange(start + ins.length,start + ins.length);
+    }
+}
diff --git a/Open-ILS/xul/staff_client/server/OpenILS/symbol_overlay.xul b/Open-ILS/xul/staff_client/server/OpenILS/symbol_overlay.xul
new file mode 100644 (file)
index 0000000..d9a4780
--- /dev/null
@@ -0,0 +1,373 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<overlay id="iisg_symbol_overlay"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+    
+    <scripts id="iisg_symbol_scripts">
+        <script type="text/javascript" src="symbol_overlay.js" />   
+        <script>dump('finished iisg_symbol_overlay\n');</script>
+    </scripts>
+
+<popupset>
+  <menupopup id="symbol-menu">
+      <menu label="West-European">
+          <menupopup>
+              <menu label="A">
+                  <menupopup>
+                    <menuitem label="À" oncommand="document.popupNode.label='\u00C0'"/>
+                    <menuitem label="à" oncommand="document.popupNode.label='\u00E0'"/>
+                    <menuitem label="Á" oncommand="document.popupNode.label='\u00C1'"/>
+                    <menuitem label="á" oncommand="document.popupNode.label='\u00E1'"/>
+                    <menuitem label="Ä" oncommand="document.popupNode.label='\u00C4'"/>
+                    <menuitem label="ä" oncommand="document.popupNode.label='\u00E4'"/>
+                    <menuitem label="Â" oncommand="document.popupNode.label='\u00C2'"/>
+                    <menuitem label="â" oncommand="document.popupNode.label='\u00E2'"/>
+                    <menuitem label="Ã" oncommand="document.popupNode.label='\u00C3'"/>
+                    <menuitem label="ã" oncommand="document.popupNode.label='\u00E3'"/>
+                    <menuitem label="Å" oncommand="document.popupNode.label='\u00C5'"/>
+                    <menuitem label="å" oncommand="document.popupNode.label='\u00E5'"/>                    
+                </menupopup>
+            </menu>
+              <menu label="E">
+                  <menupopup>
+                    <menuitem label="È" oncommand="document.popupNode.label='\u00C8'"/>
+                    <menuitem label="è" oncommand="document.popupNode.label='\u00E8'"/>
+                    <menuitem label="É" oncommand="document.popupNode.label='\u00C9'"/>
+                    <menuitem label="é" oncommand="document.popupNode.label='\u00E9'"/>
+                    <menuitem label="Ë" oncommand="document.popupNode.label='\u00CB'"/>
+                    <menuitem label="ë" oncommand="document.popupNode.label='\u00EB'"/>
+                    <menuitem label="Ê" oncommand="document.popupNode.label='\u00CA'"/>
+                    <menuitem label="ê" oncommand="document.popupNode.label='\u00EA'"/>                
+                </menupopup>
+            </menu>
+              <menu label="I">
+                  <menupopup>
+                    <menuitem label="Ì" oncommand="document.popupNode.label='\u00CC'"/>
+                    <menuitem label="ì" oncommand="document.popupNode.label='\u00EC'"/>
+                    <menuitem label="Í" oncommand="document.popupNode.label='\u00CD'"/>
+                    <menuitem label="í" oncommand="document.popupNode.label='\u00ED'"/>
+                    <menuitem label="Ï" oncommand="document.popupNode.label='\u00CF'"/>
+                    <menuitem label="ï" oncommand="document.popupNode.label='\u00EF'"/>
+                    <menuitem label="Î" oncommand="document.popupNode.label='\u00CE'"/>
+                    <menuitem label="î" oncommand="document.popupNode.label='\u00EE'"/>
+                   </menupopup>
+            </menu>
+              <menu label="O">
+                  <menupopup>
+                    <menuitem label="Ò" oncommand="document.popupNode.label='\u00D2'"/>
+                    <menuitem label="ò" oncommand="document.popupNode.label='\u00F2'"/>
+                    <menuitem label="Ó" oncommand="document.popupNode.label='\u00D3'"/>
+                    <menuitem label="ó" oncommand="document.popupNode.label='\u00F3'"/>
+                    <menuitem label="Ö" oncommand="document.popupNode.label='\u00D6'"/>
+                    <menuitem label="ö" oncommand="document.popupNode.label='\u00F6'"/>
+                    <menuitem label="Ô" oncommand="document.popupNode.label='\u00D4'"/>
+                    <menuitem label="ô" oncommand="document.popupNode.label='\u00F4'"/>
+                    <menuitem label="Õ" oncommand="document.popupNode.label='\u00D5'"/>
+                    <menuitem label="õ" oncommand="document.popupNode.label='\u00F5'"/>
+                    <menuitem label="Ø" oncommand="document.popupNode.label='\u00D8'"/>
+                    <menuitem label="ø" oncommand="document.popupNode.label='\u00F8'"/>                    
+                </menupopup>
+            </menu>
+              <menu label="U">
+                  <menupopup>
+                    <menuitem label="Ù" oncommand="document.popupNode.label='\u00D9'"/>
+                    <menuitem label="ù" oncommand="document.popupNode.label='\u00F9'"/>
+                    <menuitem label="Ú" oncommand="document.popupNode.label='\u00DA'"/>
+                    <menuitem label="ú" oncommand="document.popupNode.label='\u00FA'"/>
+                    <menuitem label="Ü" oncommand="document.popupNode.label='\u00DC'"/>
+                    <menuitem label="ü" oncommand="document.popupNode.label='\u00FC'"/>
+                    <menuitem label="Û" oncommand="document.popupNode.label='\u00DB'"/>
+                    <menuitem label="û" oncommand="document.popupNode.label='\u00FB'"/>
+                </menupopup>
+            </menu>
+              <menu label="C">
+                  <menupopup>
+                    <menuitem label="Ç" oncommand="document.popupNode.label='\u00C7'"/>
+                    <menuitem label="ç" oncommand="document.popupNode.label='\u00E7'"/>
+                 </menupopup>
+            </menu>
+              <menu label="N">
+                  <menupopup>
+                    <menuitem label="Ñ" oncommand="document.popupNode.label='\u00D1'"/>
+                    <menuitem label="ñ" oncommand="document.popupNode.label='\u00F1'"/>
+                 </menupopup>
+            </menu>                 
+        </menupopup>
+    </menu>
+    <menu label="Turkish">
+          <menupopup>
+              <menu label="A">
+                  <menupopup>
+                    <menuitem label="Â" oncommand="document.popupNode.label='\u00C2'"/>
+                    <menuitem label="â" oncommand="document.popupNode.label='\u00E2'"/>
+                </menupopup>
+            </menu>
+              <menu label="G">
+                  <menupopup>
+                    <menuitem label="Ğ" oncommand="document.popupNode.label='\u011E'"/>
+                    <menuitem label="ğ" oncommand="document.popupNode.label='\u011F'"/>
+                </menupopup>
+            </menu>
+              <menu label="I">
+                  <menupopup>
+                    <menuitem label="İ" oncommand="document.popupNode.label='\u0130'"/>
+                    <menuitem label="ı" oncommand="document.popupNode.label='\u0131'"/>
+                   </menupopup>
+            </menu>
+              <menu label="O">
+                  <menupopup>
+                    <menuitem label="Ö" oncommand="document.popupNode.label='\u00D6'"/>
+                    <menuitem label="ö" oncommand="document.popupNode.label='\u00F6'"/>
+                </menupopup>
+            </menu>
+              <menu label="U">
+                  <menupopup>
+                    <menuitem label="Ü" oncommand="document.popupNode.label='\u00DC'"/>
+                    <menuitem label="ü" oncommand="document.popupNode.label='\u00FC'"/>
+                </menupopup>
+            </menu>
+              <menu label="C">
+                  <menupopup>
+                    <menuitem label="Ç" oncommand="document.popupNode.label='\u00C7'"/>
+                    <menuitem label="ç" oncommand="document.popupNode.label='\u00E7'"/>
+                 </menupopup>
+            </menu>
+              <menu label="S">
+                  <menupopup>
+                    <menuitem label="Ş" oncommand="document.popupNode.label='\u015E'"/>
+                    <menuitem label="ş" oncommand="document.popupNode.label='\u015F'"/>
+                 </menupopup>
+            </menu>                 
+        </menupopup>
+    </menu>
+    <menu label="Cyrillic">
+          <menupopup>
+              <menu label="A">
+                  <menupopup>
+                    <menuitem label="Ă" oncommand="document.popupNode.label='\u0102'"/>
+                    <menuitem label="ă" oncommand="document.popupNode.label='\u0103'"/>
+                </menupopup>
+            </menu>
+              <menu label="I">
+                  <menupopup>
+                    <menuitem label="Ï" oncommand="document.popupNode.label='\u00CF'"/>
+                    <menuitem label="ï" oncommand="document.popupNode.label='\u00EF'"/>
+                    <menuitem label="Ī" oncommand="document.popupNode.label='\u012A'"/>
+                    <menuitem label="ī" oncommand="document.popupNode.label='\u012B'"/>
+                   </menupopup>
+            </menu>
+              <menu label="C">
+                  <menupopup>
+                    <menuitem label="Č" oncommand="document.popupNode.label='\u010C'"/>
+                    <menuitem label="č" oncommand="document.popupNode.label='\u010D'"/>
+                 </menupopup>
+            </menu>
+              <menu label="S">
+                  <menupopup>
+                    <menuitem label="Š" oncommand="document.popupNode.label='\u0160'"/>
+                    <menuitem label="š" oncommand="document.popupNode.label='\u0161'"/>
+                 </menupopup>
+            </menu>
+            <menu label="Z">
+                  <menupopup>
+                    <menuitem label="Ž" oncommand="document.popupNode.label='\u017D'"/>
+                    <menuitem label="ž" oncommand="document.popupNode.label='\u017E'"/>
+                </menupopup>
+            </menu>                 
+        </menupopup>
+    </menu>
+    <menu label="Slavic">
+          <menupopup>
+              <menu label="A">
+                  <menupopup>
+                    <menuitem label="Á" oncommand="document.popupNode.label='\u00C1'"/>
+                    <menuitem label="á" oncommand="document.popupNode.label='\u00E1'"/>
+                    <menuitem label="Ą" oncommand="document.popupNode.label='\u0104'"/>
+                    <menuitem label="ą" oncommand="document.popupNode.label='\u0105'"/>
+                </menupopup>
+            </menu>
+              <menu label="E">
+                  <menupopup>
+                    <menuitem label="É" oncommand="document.popupNode.label='\u00C9'"/>
+                    <menuitem label="é" oncommand="document.popupNode.label='\u00E9'"/>
+                    <menuitem label="Ĕ" oncommand="document.popupNode.label='\u0114'"/>
+                    <menuitem label="ĕ" oncommand="document.popupNode.label='\u0115'"/>
+                    <menuitem label="Ę" oncommand="document.popupNode.label='\u0118'"/>
+                    <menuitem label="ę" oncommand="document.popupNode.label='\u0119'"/>
+                   </menupopup>
+            </menu>
+              <menu label="I">
+                  <menupopup>
+                    <menuitem label="Í" oncommand="document.popupNode.label='\u00CD'"/>
+                    <menuitem label="í" oncommand="document.popupNode.label='\u00ED'"/>
+                 </menupopup>
+            </menu>
+              <menu label="O">
+                  <menupopup>
+                    <menuitem label="Ó" oncommand="document.popupNode.label='\u00D3'"/>
+                    <menuitem label="ó" oncommand="document.popupNode.label='\u00F3'"/>
+                 </menupopup>
+            </menu>
+            <menu label="U">
+                  <menupopup>
+                    <menuitem label="Ů" oncommand="document.popupNode.label='\u016E'"/>
+                    <menuitem label="ů" oncommand="document.popupNode.label='\u016F'"/>
+                </menupopup>
+            </menu>
+            <menu label="Y">
+                  <menupopup>
+                    <menuitem label="Ý" oncommand="document.popupNode.label='\u00DD'"/>
+                    <menuitem label="ý" oncommand="document.popupNode.label='\u00FD'"/>
+                </menupopup>
+            </menu>    
+            <menu label="L">
+                  <menupopup>
+                    <menuitem label="Ł" oncommand="document.popupNode.label='\u0141'"/>
+                    <menuitem label="ł" oncommand="document.popupNode.label='\u0142'"/>
+                </menupopup>
+            </menu>               
+            <menu label="C">
+                  <menupopup>
+                    <menuitem label="Ć" oncommand="document.popupNode.label='\u0106'"/>
+                    <menuitem label="ć" oncommand="document.popupNode.label='\u0107'"/>
+                    <menuitem label="Č" oncommand="document.popupNode.label='\u010C'"/>
+                    <menuitem label="č" oncommand="document.popupNode.label='\u010D'"/>
+                </menupopup>
+            </menu>               
+            <menu label="D">
+                  <menupopup>
+                    <menuitem label="Ð" oncommand="document.popupNode.label='\u00D0'"/>
+                    <menuitem label="ð" oncommand="document.popupNode.label='\u00F0'"/>
+                </menupopup>
+            </menu>
+            <menu label="N">
+                  <menupopup>
+                    <menuitem label="Ń" oncommand="document.popupNode.label='\u0143'"/>
+                    <menuitem label="ń" oncommand="document.popupNode.label='\u0144'"/>
+                </menupopup>
+            </menu>
+            <menu label="R">
+                  <menupopup>
+                    <menuitem label="Ř" oncommand="document.popupNode.label='\u0158'"/>
+                    <menuitem label="ř" oncommand="document.popupNode.label='\u0159'"/>
+                </menupopup>
+            </menu>
+            <menu label="S">
+                  <menupopup>
+                    <menuitem label="Ś" oncommand="document.popupNode.label='\u015A'"/>
+                    <menuitem label="ś" oncommand="document.popupNode.label='\u015B'"/>
+                </menupopup>
+            </menu>
+            <menu label="Z">
+                  <menupopup>
+                    <menuitem label="Ž" oncommand="document.popupNode.label='\u017D'"/>
+                    <menuitem label="ž" oncommand="document.popupNode.label='\u017E'"/>
+                    <menuitem label="Ź" oncommand="document.popupNode.label='\u0179'"/>
+                    <menuitem label="ź" oncommand="document.popupNode.label='\u017A'"/>
+                    <menuitem label="Ż" oncommand="document.popupNode.label='\u017B'"/>
+                    <menuitem label="ż" oncommand="document.popupNode.label='\u017C'"/>
+                </menupopup>
+            </menu>
+        </menupopup>
+    </menu>
+    <menu label="Hungarian">
+          <menupopup>
+              <menu label="A">
+                  <menupopup>
+                    <menuitem label="Á" oncommand="document.popupNode.label='\u00C1'"/>
+                    <menuitem label="á" oncommand="document.popupNode.label='\u00E1'"/>
+                </menupopup>
+            </menu>
+              <menu label="E">
+                  <menupopup>
+                    <menuitem label="É" oncommand="document.popupNode.label='\u00C9'"/>
+                    <menuitem label="é" oncommand="document.popupNode.label='\u00E9'"/>
+                   </menupopup>
+            </menu>
+              <menu label="I">
+                  <menupopup>
+                    <menuitem label="Í" oncommand="document.popupNode.label='\u00CD'"/>
+                    <menuitem label="í" oncommand="document.popupNode.label='\u00ED'"/>
+                 </menupopup>
+            </menu>
+              <menu label="O">
+                  <menupopup>
+                    <menuitem label="Ó" oncommand="document.popupNode.label='\u00D3'"/>
+                    <menuitem label="ó" oncommand="document.popupNode.label='\u00F3'"/>
+                    <menuitem label="Ö" oncommand="document.popupNode.label='\u00D3'"/>
+                    <menuitem label="ö" oncommand="document.popupNode.label='\u00F3'"/>
+                    <menuitem label="Ő" oncommand="document.popupNode.label='\u00D3'"/>
+                    <menuitem label="ő" oncommand="document.popupNode.label='\u00F3'"/>
+                 </menupopup>
+            </menu>
+            <menu label="U">
+                  <menupopup>
+                    <menuitem label="Ú" oncommand="document.popupNode.label='\u00DA'"/>
+                    <menuitem label="ú" oncommand="document.popupNode.label='\u00FA'"/>
+                    <menuitem label="Ü" oncommand="document.popupNode.label='\u00DC'"/>
+                    <menuitem label="ü" oncommand="document.popupNode.label='\u00FC'"/>
+                    <menuitem label="Ű" oncommand="document.popupNode.label='\u0170'"/>
+                    <menuitem label="ű" oncommand="document.popupNode.label='\u0171'"/>
+                </menupopup>
+            </menu>
+        </menupopup>
+    </menu>
+    
+  </menupopup>
+</popupset>
+        <popupset>
+            <panel id="symbol-panel" noautohide="false" titlebar="normal" onpopuphidden="" >
+                  <vbox>
+                    <hbox>
+                        <button class="sym" style="width:2em;min-width:1em;" id="sym1A" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="À" />
+                        <button class="sym" id="sym1B" context="symbol-menu" onclick="ret(label,event);"  oils_persist="label" label="Á" />
+                        <button class="sym" id="sym1C" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="Ä" />
+                        <button class="sym" id="sym1D" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="Â" />
+                        <button class="sym" id="sym1E" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="Ã" />
+                        <button class="sym" id="sym1F" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="Å" />
+                    </hbox>
+                    <hbox>
+                        <button class ="sym" id="sym2A" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="à"/>
+                        <button class ="sym" id="sym2B" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="á"/>
+                        <button class ="sym" id="sym2C" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="ä"/>
+                        <button class ="sym" id="sym2D" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="â"/>
+                        <button class ="sym" id="sym2E" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="ã" />
+                        <button class ="sym" id="sym2F" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="å" />
+                    </hbox>        
+                    <hbox>
+                        <button class ="sym" id="sym3A" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="Ò"/>
+                        <button class ="sym" id="sym3B" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="Ó"/>
+                        <button class ="sym" id="sym3C" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="Ö"/>
+                        <button class ="sym" id="sym3D" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="Ô"/>
+                        <button class ="sym" id="sym3E" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="Õ" />
+                        <button class ="sym" id="sym3F" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="Ø" />                    
+                    </hbox>
+                    <hbox>
+                        <button class ="sym" id="sym4A" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="ò"/>
+                        <button class ="sym" id="sym4B" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="ó"/>
+                        <button class ="sym" id="sym4C" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="ö"/>
+                        <button class ="sym" id="sym4D" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="ô"/>
+                        <button class ="sym" id="sym4E" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="õ" />
+                        <button class ="sym" id="sym4F" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="ø" />                    
+                    </hbox>
+                    <hbox>
+                        <button class ="sym" id="sym5A" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="È"/>
+                        <button class ="sym" id="sym5B" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="É"/>
+                        <button class ="sym" id="sym5C" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="Ë"/>
+                        <button class ="sym" id="sym5D" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="Ê"/>
+                        <button class ="sym" id="sym5E" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="Ç" />
+                        <button class ="sym" id="sym5F" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="Ñ" />                
+                    </hbox>
+                    <hbox>
+                        <button class="sym" id="sym6A" context="symbol-menu" onclick="ret(label,event);"  oils_persist="label" label="è"/>
+                        <button class ="sym" id="sym6B" context="symbol-menu" onclick="ret(label,event);"  oils_persist="label" label="é"/>
+                        <button class ="sym" id="sym6C" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="ë"/>
+                        <button class ="sym" id="sym6D" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="ê"/>
+                        <button class ="sym" id="sym6E" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="ç" />
+                        <button class ="sym" id="sym6F" context="symbol-menu" onclick="ret(label,event);" oils_persist="label" label="ñ" />                
+                    </hbox>                        
+                  </vbox>
+            </panel>
+        </popupset>
+</overlay>
+
index fbd1dd6..242acdc 100644 (file)
@@ -16,6 +16,9 @@
                 try {
                     if (typeof level == 'undefined') { level = 4; }
                     if (level > _dump_level) { return; }
+                    if (typeof _dump_prefix != 'undefined') {
+                        _original_dump(_dump_prefix + ' ');
+                    }
                     switch(level) {
                         case 1: case 'error': _original_dump('error: '); break;
                         case 2: case 'warn': _original_dump('warn: '); break;
@@ -58,6 +61,9 @@
         <script type="text/javascript" src="/xul/server/util/text.js" />
         <script type="text/javascript" src="/xul/server/util/widgets.js" />
         <script type="text/javascript" src="/xul/server/util/window.js" />
+        <script type="text/javascript" src="/xul/server/circ/util.js" />
+        <script type="text/javascript" src="/xul/server/cat/util.js" />
+        <script type="text/javascript" src="/xul/server/patron/util.js" />
         <script type="text/javascript" src="/opac/common/js/utils.js" />
         <script type="text/javascript" src="/opac/common/js/CGI.js" />
         <script type="text/javascript" src="/opac/common/js/md5.js" />
index 82f5de0..8c3e389 100644 (file)
@@ -67,10 +67,13 @@ g.printer_settings = function() {
         print_silent_pref = g.prefs.getBoolPref('print.always_print_silent');
     }
     g.prefs.setBoolPref('print.always_print_silent', false);
+    g.prefs.clearUserPref('print.always_print_silent');
     var w = get_contentWindow(document.getElementById('sample'));
     g.print.NSPrint(w ? w : window, false, {});
     g.print.save_settings();
-    g.prefs.setBoolPref('print.always_print_silent', print_silent_pref);
+    if (print_silent_pref) {
+        g.prefs.setBoolPref('print.always_print_silent', true);
+    }
 }
 
 g.set_print_strategy = function(which) {
index ae896a7..55bad58 100644 (file)
@@ -52,7 +52,7 @@ function scSetPerms() {
     PERMS[ASSET].update_stat_cat_entry =  OILS_WORK_PERMS.UPDATE_COPY_STAT_CAT_ENTRY;
     PERMS[ASSET].delete_stat_cat_entry =  OILS_WORK_PERMS.DELETE_COPY_STAT_CAT_ENTRY;
 
-    // set up the fitler select
+    // set up the filter select
     var fselector = $('sc_org_filter');
     var org_list = PERMS[currentlyVisible].update_stat_cat;
     buildMergedOrgSel(fselector, org_list, 0, 'shortname');
@@ -296,10 +296,13 @@ function scBuildNew() {
     var org_list = PERMS[type].create_stat_cat;
     if(org_list.length == 0) { /* no create perms */
         $('sc_new').disabled = true;
-        typeSel.disabled = true;
         libSel.disabled = true;
         return;
     }
+    else {
+        $('sc_new').disabled = false;
+        libSel.disabled = false;
+    }
     buildMergedOrgSel(libSel, org_list, 0, 'shortname');
 }
 
index db87e5b..44bc145 100644 (file)
@@ -147,8 +147,14 @@ function add_volumes() {
 
         var title = document.getElementById('offlineStrings').getFormattedString('staff.circ.copy_status.add_volumes.title', [docid]);
 
-        var horizontal_interface = String( g.data.hash.aous['ui.cat.volume_copy_editor.horizontal'] ) == 'true';
-        var url = window.xulG.url_prefix( horizontal_interface ? urls.XUL_VOLUME_COPY_CREATOR_HORIZONTAL : urls.XUL_VOLUME_COPY_CREATOR );
+        var url;
+        var unified_interface = String( g.data.hash.aous['ui.unified_volume_copy_editor'] ) == 'true';
+        if (unified_interface) {
+            var horizontal_interface = String( g.data.hash.aous['ui.cat.volume_copy_editor.horizontal'] ) == 'true';
+            url = window.xulG.url_prefix( horizontal_interface ? urls.XUL_VOLUME_COPY_CREATOR_HORIZONTAL : urls.XUL_VOLUME_COPY_CREATOR );
+        } else {
+            url = window.xulG.url_prefix( urls.XUL_VOLUME_COPY_CREATOR_ORIGINAL );
+        }
         var w = xulG.new_tab(
             url,
             { 'tab_name' : title },
index 6aa7241..8c0ddaf 100644 (file)
@@ -299,8 +299,15 @@ cat.copy_browser.prototype = {
 
                                     var title = document.getElementById('catStrings').getString('staff.cat.copy_browser.add_item.title');
 
-                                    var horizontal_interface = String( obj.data.hash.aous['ui.cat.volume_copy_editor.horizontal'] ) == 'true';
-                                    var url = window.xulG.url_prefix( horizontal_interface ? urls.XUL_VOLUME_COPY_CREATOR_HORIZONTAL : urls.XUL_VOLUME_COPY_CREATOR );
+                                    var url;
+                                    var unified_interface = String( obj.data.hash.aous['ui.unified_volume_copy_editor'] ) == 'true';
+                                    if (unified_interface) {
+                                        var horizontal_interface = String( obj.data.hash.aous['ui.cat.volume_copy_editor.horizontal'] ) == 'true';
+                                        url = xulG.url_prefix( horizontal_interface ? urls.XUL_VOLUME_COPY_CREATOR_HORIZONTAL : urls.XUL_VOLUME_COPY_CREATOR );
+                                    } else {
+                                        url = xulG.url_prefix( urls.XUL_VOLUME_COPY_CREATOR_ORIGINAL );
+                                    }
+
                                     var w = xulG.new_tab(
                                         url,
                                         { 'tab_name' : title },
@@ -348,6 +355,12 @@ cat.copy_browser.prototype = {
                             ['command'],
                             function() {
                                 try {
+                                    var unified_interface = String( obj.data.hash.aous['ui.unified_volume_copy_editor'] ) == 'true';
+                                    if (!unified_interface) {
+                                        obj.controller.control_map['old_cmd_edit_items'][1]();
+                                        return;
+                                    }
+
                                     JSAN.use('util.functional');
 
                                     var list = util.functional.filter_list(
index 4810959..3a459a2 100644 (file)
                                 } else {
                                     $w('copy_summary_callnumber',cn.label());    
                                 }
-                                g.list.append({'row':{'my':{'acp':copy,'acn':cn}}});
+                                g.list.append({'row':{'my':{'acp':copy,'acn':cn,'circ':xulG.circ}}});
                                 g.barcode = copy.barcode(); g.doc_id = cn.record();
                                 if (g.doc_id > -1) {
                                     $('show_in_opac').hidden = false;
index 3f4b1cf..3d28729 100644 (file)
@@ -9,6 +9,7 @@
 ]>
 
 <?xul-overlay href="/xul/server/OpenILS/util_overlay.xul"?>
+<?xul-overlay href="/xul/server/OpenILS/symbol_overlay.xul"?>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:xhtml="http://www.w3.org/1999/xhtml" onload="try { my_init(); font_helper(); persist_helper(); } catch(E) { alert(E); }">
 
 </groupbox>
 
 <hbox hidden="true" id="text-editor" flex="1">
-    <xhtml:textarea rows="50" cols='100' onkeypress="netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); if (!(event.altKey || event.ctrlKey || event.metaKey)) { oils_lock_page(); }" id="text-editor-box"></xhtml:textarea>
+    <xhtml:textarea rows="50" cols='100' onkeypress="if (event.charCode == 115 &amp;&amp; event.ctrlKey &amp;&amp; $('symbol-panel')) { setNod(tab); $('symbol-panel').openPopup(tab, 'after_pointer'); } else { netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); if (!(event.altKey || event.ctrlKey || event.metaKey)) { oils_lock_page(); } }" context="clipboard" id="text-editor-box"></xhtml:textarea>
 </hbox>
 
 <grid name="authority-marc-template" hidden="true">
index e394dfb..1aea205 100644 (file)
@@ -448,6 +448,7 @@ g.render_callnumber_copy_count_entry = function(row,ou_id,count) {
                         ,false
                     );
                     classification_column_box.appendChild(classification_column_menulist);
+                    classification_column_menulist.value = g.label_class;
 
                     /**** PREFIX COLUMN revisited ****/
                     var prefix_column_menulist = g.render_prefix_menu(call_number_column_textbox);
@@ -776,11 +777,10 @@ g.delay_gather_copies_soon = function() {
     }
 }
 
-g.gather_copies_soon = function() {
+g.gather_copies_soon = function(ev) {
     try {
         if (!xulG.unified_interface) { return; }
         dump('g.gather_copies_soon()\n');
-        document.getElementById("Create").disabled = true;
         if (g.update_copy_editor_timeoutID) {
             clearTimeout(g.update_copy_editor_timeoutID);
         }
@@ -791,9 +791,8 @@ g.gather_copies_soon = function() {
                 try {
                     g.gather_copies();
                     xulG.refresh_copy_editor();
-                    document.getElementById("Create").disabled = false;
                 } catch(E) {
-                    alert('Error in volume_copy_editor.js with g.gather_copies_soon setTimeout func(): ' + E);
+                    dump('Error in volume_copy_editor.js with g.gather_copies_soon setTimeout func(): ' + E + '\n');
                 }
             }, update_timer
         );
@@ -876,6 +875,9 @@ g.gather_copies = function() {
 
         for (var i = 0; i < barcodes.length; i++) {
             var acp_id = barcodes[i].getAttribute('acp_id') || g.new_acp_id--;
+            if (acp_id < 0) {
+                barcodes[i].setAttribute('acp_id',acp_id);
+            }
             var ou_id = barcodes[i].getAttribute('ou_id');
             var callnumber_composite_key = barcodes[i].getAttribute('callkey');
             var barcode = barcodes[i].value;
@@ -1075,12 +1077,17 @@ g.stash_and_close = function(param) {
 
     try {
 
+        if (g.update_copy_editor_timeoutID) {
+            clearTimeout(g.update_copy_editor_timeoutID);
+        }
+
         var copies;
         if (xulG.unified_interface) {
+            g.gather_copies();
+            xulG.refresh_copy_editor();
             copies = xulG.copies;
         } else {
             copies = g.gather_copies();
-            copies = blob.copies;
         }
 
         var dont_close = false;
index 7997ab5..9a36d7e 100644 (file)
@@ -537,6 +537,7 @@ circ.checkin.prototype = {
                 'barcode' : barcode,
                 'disable_textbox' : function() { 
                     if (!async) {
+                        textbox.blur();
                         textbox.disabled = true; 
                         textbox.setAttribute('disabled', 'true'); 
                     }
@@ -544,9 +545,11 @@ circ.checkin.prototype = {
                 'enable_textbox' : function() { 
                     textbox.disabled = false; 
                     textbox.setAttribute('disabled', 'false'); 
+                    textbox.focus();
                 },
                 'checkin_result' : function(checkin) {
                     textbox.disabled = false;
+                    textbox.focus();
                     //obj.controller.view.cmd_checkin_submit_barcode.setAttribute('disabled', 'false'); 
                     obj.checkin2(checkin,backdate,row_params);
                 },
index e208d5b..522a740 100644 (file)
         label="&staff.checkin.print_receipt.label;" 
         command="cmd_checkin_print"
         accesskey="&staff.checkin.print_receipt.accesskey;"/>
+    <checkbox id="printer_prompt" label="&staff.circ.checkin_overlay.printer_prompt.label;" checked="true" oils_persist="checked"/>
     <checkbox id="trim_list" label="&staff.circ.checkin_overlay.trim_list.label;" checked="true" oils_persist="checked"/> 
     <checkbox id="async_checkin" label="&staff.circ.checkin_overlay.async_checkin.label;" checked="false" oils_persist="checked"/> 
     <checkbox id="strict_barcode" label="&staff.circ.checkin_overlay.strict_barcode.label;" checked="false" oils_persist="checked"/> 
index 9849b40..4227858 100644 (file)
@@ -119,6 +119,7 @@ circ.checkout.prototype = {
                         ['change'],
                         function(ev) { 
                             try {
+                               document.getElementById('checkout_duedate_checkbox').checked = true;
                                 if (obj.check_date(ev.target)) {
                                     ev.target.parentNode.setAttribute('style','');
                                 } else {
index f94c6a1..bff79af 100644 (file)
@@ -544,8 +544,15 @@ circ.copy_status.prototype = {
     
                                     var title = document.getElementById('circStrings').getFormattedString('staff.circ.copy_status.add_items.title', [r]);
     
-                                    var horizontal_interface = String( obj.data.hash.aous['ui.cat.volume_copy_editor.horizontal'] ) == 'true';
-                                    var url = window.xulG.url_prefix( horizontal_interface ? urls.XUL_VOLUME_COPY_CREATOR_HORIZONTAL : urls.XUL_VOLUME_COPY_CREATOR );
+                                    var url;
+                                    var unified_interface = String( obj.data.hash.aous['ui.unified_volume_copy_editor'] ) == 'true';
+                                    if (unified_interface) {
+                                        var horizontal_interface = String( obj.data.hash.aous['ui.cat.volume_copy_editor.horizontal'] ) == 'true';
+                                        url = window.xulG.url_prefix( horizontal_interface ? urls.XUL_VOLUME_COPY_CREATOR_HORIZONTAL : urls.XUL_VOLUME_COPY_CREATOR );
+                                    } else {
+                                        url = window.xulG.url_prefix( urls.XUL_VOLUME_COPY_CREATOR_ORIGINAL );
+                                    }
+
                                     var w = xulG.new_tab(
                                         url,
                                         { 'tab_name' : title },
@@ -696,8 +703,15 @@ circ.copy_status.prototype = {
 
                                     var title = document.getElementById('circStrings').getFormattedString('staff.circ.copy_status.add_volumes.title', [r]);
 
-                                    var horizontal_interface = String( obj.data.hash.aous['ui.cat.volume_copy_editor.horizontal'] ) == 'true';
-                                    var url = window.xulG.url_prefix( horizontal_interface ? urls.XUL_VOLUME_COPY_CREATOR_HORIZONTAL : urls.XUL_VOLUME_COPY_CREATOR );
+                                    var url;
+                                    var unified_interface = String( obj.data.hash.aous['ui.unified_volume_copy_editor'] ) == 'true';
+                                    if (unified_interface) {
+                                        var horizontal_interface = String( obj.data.hash.aous['ui.cat.volume_copy_editor.horizontal'] ) == 'true';
+                                        url = window.xulG.url_prefix( horizontal_interface ? urls.XUL_VOLUME_COPY_CREATOR_HORIZONTAL : urls.XUL_VOLUME_COPY_CREATOR );
+                                    } else {
+                                        url = window.xulG.url_prefix( urls.XUL_VOLUME_COPY_CREATOR_ORIGINAL );
+                                    }
+
                                     var w = xulG.new_tab(
                                         url,
                                         { 'tab_name' : title },
index d25552c..eb4cade 100644 (file)
@@ -94,7 +94,7 @@
     <messagecatalog id="circStrings" src="/xul/server/locale/<!--#echo var='locale'-->/circ.properties" />
     <messagecatalog id="catStrings" src="/xul/server/locale/<!--#echo var='locale'-->/cat.properties" />
 
-    <groupbox>
+    <groupbox flex="1" style="overflow: auto">
         <caption label="&staff.circ.pre_cat.caption.label;"/>
         <grid>
             <columns><column /><column flex="1"/></columns>
index 3e13c55..14bffab 100644 (file)
@@ -2808,7 +2808,7 @@ circ.util.checkin_via_barcode2 = function(session,params,backdate,auto_print,che
                                 print.simple( msg , { 'no_prompt' : true, 'content_type' : 'text/html' } );
                             } else {
                                 var template = 'hold_slip';
-                                var params = {
+                                var parms = {
                                     'patron' : print_data.user,
                                     'lib' : data.hash.aou[ check.payload.hold.pickup_lib() ],
                                     'staff' : data.list.au[0],
@@ -2819,7 +2819,10 @@ circ.util.checkin_via_barcode2 = function(session,params,backdate,auto_print,che
                                     'list' : print_list,
                                     'data' : print_data
                                 };
-                                print.tree_list( params );
+                                if ($('printer_prompt')) {
+                                    if (! $('printer_prompt').checked) { parms.no_prompt = true; }
+                                }
+                                print.tree_list( parms );
                             }
                         } catch(E) {
                             var err_msg = document.getElementById('commonStrings').getString('common.error');
@@ -3198,7 +3201,7 @@ circ.util.checkin_via_barcode2 = function(session,params,backdate,auto_print,che
                         print.simple( msg , { 'no_prompt' : true, 'content_type' : 'text/html' } );
                     } else {
                         var template = check.payload.hold ? 'hold_transit_slip' : 'transit_slip';
-                        var params = {
+                        var parms = {
                             'patron' : print_data.user,
                             'lib' : data.hash.aou[ data.list.au[0].ws_ou() ],
                             'staff' : data.list.au[0],
@@ -3209,7 +3212,10 @@ circ.util.checkin_via_barcode2 = function(session,params,backdate,auto_print,che
                             'list' : print_list,
                             'data' : print_data 
                         };
-                        print.tree_list( params );
+                        if ($('printer_prompt')) {
+                            if (! $('printer_prompt').checked) { parms.no_prompt = true; }
+                        }
+                        print.tree_list( parms );
                     }
                 } catch(E) {
                     var err_msg = document.getElementById('commonStrings').getString('common.error');
index 6f8a748..44b6c81 100644 (file)
                 function hold_pull_list(newtab) {
                         var loc = urls.XUL_REMOTE_BROWSER + '?url=' + window.escape(urls.XUL_HOLD_PULL_LIST + '?ses=' + window.escape(ses()));
                         var params = {'tab_name':'On Shelf Pull List'};
-                        var content_params = {'show_print_button':true};
                         
                         if(newtab)
-                                xulG.new_tab(loc, params, content_params);
+                                xulG.new_tab(loc, params);
                         else
-                                xulG.set_tab(loc, params, content_params);
+                                xulG.set_tab(loc, params);
                 }
                 function checkout(newtab) {
                         if(newtab)
index b0453b6..12463fe 100644 (file)
@@ -26,6 +26,10 @@ staff.serial.sdist_editor.create.accesskey=C
 staff.serial.sdist_editor.modify.label=Modify Distribution(s)
 staff.serial.sdist_editor.modify.accesskey=M
 staff.serial.sdist_editor.notes=Distribution Notes
+staff.serial.sdist_editor.add_to_sre.label=Add to record entry
+staff.serial.sdist_editor.merge_with_sre.label=Merge with record entry
+staff.serial.sdist_editor.use_sre_only.label=Use record entry only
+staff.serial.sdist_editor.use_sdist_only.label=Do not use record entry
 staff.serial.siss_editor.count=1 issuance
 staff.serial.siss_editor.count.plural=%1$s issuances
 staff.serial.siss_editor.create.label=Create Issuance(s)
@@ -59,6 +63,8 @@ staff.serial.manage_dists.delete_sstr.confirm=Are you sure you would like to del
 staff.serial.manage_dists.delete_sstr.confirm.plural=Are you sure you would like to delete these %1$s streams?
 staff.serial.manage_dists.delete_sstr.title=Delete Streams?
 staff.serial.manage_dists.delete_sstr.override=Override Delete Failure? Doing so will delete all attached items as well!
+staff.serial.manage_items.subscriber.label=Subscriber
+staff.serial.manage_items.holder.label=Holder
 staff.serial.manage_subs.add.error=error adding object in manage_subs.js:
 staff.serial.manage_subs.delete.error=error deleting object in manage_subs.js:
 staff.serial.manage_subs.delete_scap.confirm=Are you sure you would like to delete this caption and pattern?
index 8ad14be..56627e0 100644 (file)
@@ -91,7 +91,7 @@ function retrieve_circ() {
                 var copy_summary = document.createElement('iframe'); csb.appendChild(copy_summary);
                 copy_summary.setAttribute('src',urls.XUL_COPY_SUMMARY); // + '?copy_id=' + r_circ.target_copy());
                 copy_summary.setAttribute('flex','1');
-                get_contentWindow(copy_summary).xulG = { 'copy_id' : r_circ.target_copy(), 'new_tab' : xulG.new_tab, 'url_prefix' : xulG.url_prefix };
+                get_contentWindow(copy_summary).xulG = { 'circ' : r_circ, 'copy_id' : r_circ.target_copy(), 'new_tab' : xulG.new_tab, 'url_prefix' : xulG.url_prefix };
 
                 g.network.simple_request(
                     'MODS_SLIM_RECORD_RETRIEVE_VIA_COPY.authoritative',
index ba9956e..0f7f9ac 100644 (file)
@@ -137,6 +137,8 @@ patron.display.prototype = {
                             obj.controller.view.cmd_search_form.setAttribute('disabled','true');
                             obj.left_deck.node.selectedIndex = 0;
                             obj.controller.view.patron_name.setAttribute('value', $("patronStrings").getString('staff.patron.display.cmd_search_form.no_patron'));
+                            obj.controller.view.patron_name.setAttribute('tooltiptext', '');
+                            obj.controller.view.patron_name.setAttribute('onclick', '');
                             removeCSSClass(document.documentElement,'PATRON_HAS_BILLS');
                             removeCSSClass(document.documentElement,'PATRON_HAS_OVERDUES');
                             removeCSSClass(document.documentElement,'PATRON_HAS_NOTES');
@@ -516,6 +518,7 @@ patron.display.prototype = {
                                     ]
                                 );
                                 e.setAttribute('tooltiptext',tooltiptext);
+                                e.setAttribute('onclick','try { copy_to_clipboard(event); } catch(E) { alert(E); }');
                             };
                         }
                     ],
index edf589b..085c54a 100644 (file)
@@ -238,6 +238,7 @@ patron.holds.prototype = {
                                     n.setAttribute('toggle','0');
                                     n.setAttribute('label', document.getElementById("circStrings").getString('staff.circ.holds.alt_view.label'));
                                     n.setAttribute('accesskey', document.getElementById("circStrings").getString('staff.circ.holds.alt_view.accesskey'));
+                                    obj.controller.view.save_columns.setAttribute('disabled','false');
                                 } else {
                                     document.getElementById('deck').selectedIndex = 1;
                                     n.setAttribute('toggle','1');
@@ -253,6 +254,7 @@ patron.holds.prototype = {
                                     f.xulG = xulG;
                                     f.xulG.clear_and_retrieve = function() { obj.clear_and_retrieve(); };
                                     f.fetch_and_render_all(true);
+                                    obj.controller.view.save_columns.setAttribute('disabled','true');
                                 }
                             } catch(E) {
                                 alert('Error in holds.js, cmd_alt_view handler: ' + E);
index 0c540e1..a0d7deb 100644 (file)
         <row id="pdcgpr1">
             <label id="PatronSummaryContact_day_phone_label" class="copyable text_left phone label day_phone"
                 value="&staff.patron_display.day_phone.label;" />
-            <description id="patron_day_phone" class="copyable phone value day_phone"/> 
+            <description id="patron_day_phone" class="copyable phone value day_phone click_link" onclick="try { copy_to_clipboard(event); } catch(E) { alert(E); }"/> 
         </row>
         <row id="pdcgpr2">
             <label id="PatronSummaryContact_evening_phone_label" class="copyable text_left phone label evening_phone"
                 value="&staff.patron_display.evening_phone.label;" />
-            <description id="patron_evening_phone" class="copyable phone value evening_phone"/>
+            <description id="patron_evening_phone" class="copyable phone value evening_phone click_link" onclick="try { copy_to_clipboard(event); } catch(E) { alert(E); }"/>
         </row>
         <row id="pdcgpr3">
             <label id="PatronSummaryContact_other_phone_label" class="copyable text_left phone label other_phone"
                 value="&staff.patron_display.other_phone.label;" />
-            <description id="patron_other_phone" class="copyable phone value other_phone"/> 
+            <description id="patron_other_phone" class="copyable phone value other_phone click_link" onclick="try { copy_to_clipboard(event); } catch(E) { alert(E); }"/> 
         </row>
         <row id="pdsgpr4"><label id="pdsgpr4l" value=" "/></row>
         <row id="pdsgpr4a">
index 5b2e9c2..3687fff 100644 (file)
                 <row id="pdcgpr1">
                     <label id="PatronSummaryContact_day_phone_label" class="copyable text_left phone label day_phone"
                         value="&staff.patron_display.day_phone.label;" />
-                    <description id="patron_day_phone" class="copyable phone value day_phone"/> 
+                    <description id="patron_day_phone" class="copyable phone value day_phone click_link" onclick="try { copy_to_clipboard(event); } catch(E) { alert(E); }"/> 
                 </row>
                 <row id="pdcgpr2">
                     <label id="PatronSummaryContact_evening_phone_label" class="copyable text_left phone label evening_phone"
                         value="&staff.patron_display.evening_phone.label;" />
-                    <description id="patron_evening_phone" class="copyable phone value evening_phone"/>
+                    <description id="patron_evening_phone" class="copyable phone value evening_phone click_link" onclick="try { copy_to_clipboard(event); } catch(E) { alert(E); }"/>
                 </row>
                 <row id="pdcgpr3">
                     <label id="PatronSummaryContact_other_phone_label" class="copyable text_left phone label other_phone"
                         value="&staff.patron_display.other_phone.label;" />
-                    <description id="patron_other_phone" class="copyable phone value other_phone"/> 
+                    <description id="patron_other_phone" class="copyable phone value other_phone click_link" onclick="try { copy_to_clipboard(event); } catch(E) { alert(E); }"/> 
                 </row>
             </rows>
         </grid>
index b7cfc3e..1072ad4 100644 (file)
@@ -22,11 +22,17 @@ serial.manage_items.prototype = {
                var obj = this;
 
         try {
-            obj.holding_lib = $('serial_item_lib_menu').value;
+            obj.lib = $('serial_item_lib_menu').value;
+            var sdist_retrieve_params = {"+ssub":{"record_entry" : obj.docid}};
+            if (obj.mode == 'receive') {
+                sdist_retrieve_params["+ssub"].owning_lib = obj.lib;
+            } else {
+                sdist_retrieve_params.holding_lib = obj.lib;
+            }
             var robj = obj.network.request(
                 'open-ils.pcrud',
                 'open-ils.pcrud.id_list.sdist',
-                [ ses(), {"holding_lib" : obj.holding_lib, "+ssub":{"record_entry" : obj.docid}}, {"join":"ssub"} ]
+                [ ses(), sdist_retrieve_params, {"join":"ssub"} ]
             );
             if (robj != null) {
                 if (typeof robj.ilsevent != 'undefined') throw(robj);
@@ -105,8 +111,10 @@ serial.manage_items.prototype = {
         // deal with mode radio selectedIndex, as load_attributes is setting a "read-only" value
         if ($('mode_receive').getAttribute('selected')) {
             $('serial_manage_items_mode').selectedIndex = 0;
-        } else {
+        } else if ($('mode_advanced_receive').getAttribute('selected')) {
             $('serial_manage_items_mode').selectedIndex = 1;
+        } else {
+            $('serial_manage_items_mode').selectedIndex = 2;
         }
 
         // setup recent sunits list
@@ -298,16 +306,7 @@ serial.manage_items.prototype = {
                         function(evt) {
                             try {
                                 var target = evt.explicitOriginalTarget;
-                                var label = target.label;
-                                var sunit_id = target.getAttribute('sunit_id');
-                                var sdist_id = target.getAttribute('sdist_id');
-                                var sstr_id = target.getAttribute('sstr_id');
-                                obj.set_sunit(sunit_id, label, sdist_id, sstr_id);
-                                obj.save_sunit(sunit_id, label, sdist_id, sstr_id);
-                                if (obj.mode == 'bind') {
-                                    obj.refresh_list('main');
-                                    obj.refresh_list('workarea');
-                                }
+                                obj.process_unit_selection(target);
                             } catch(E) {
                                 obj.error.standard_unexpected_error_alert('cmd_set_sunit failed!',E);
                             }
@@ -348,7 +347,7 @@ serial.manage_items.prototype = {
                                     );
 
                                 var method; var success_label;
-                                if (obj.mode == 'receive') {
+                                if (obj.mode == 'receive' || obj.mode == 'advanced_receive') {
                                     method = 'open-ils.serial.receive_items';
                                     success_label = 'received';
                                 } else { // bind mode
@@ -359,7 +358,7 @@ serial.manage_items.prototype = {
                                 // deal with barcodes and call numbers for *NEW* units
                                 var barcodes = {};
                                 var call_numbers = {};
-                                var call_numbers_by_issuance_id = {};
+                                var call_numbers_by_siss_and_sdist = {};
 
                                 if (obj.current_sunit_id < 0) { // **AUTO** or **NEW** units
                                     var new_unit_barcode = '';
@@ -388,16 +387,24 @@ serial.manage_items.prototype = {
                                             alert('Invalid barcode entered, defaulting to system-generated.');
                                             barcode = '@@AUTO';
                                         } else {
+                                            // disable alarm sound temporarily
+                                            var sound_setting = obj.data.no_sound;
+                                            if (!sound_setting) { // undefined or false
+                                                obj.data.no_sound = true; obj.data.stash('no_sound');
+                                            }
                                             var test = obj.network.simple_request('FM_ACP_RETRIEVE_VIA_BARCODE',[ barcode ]);
                                             if (typeof test.ilsevent == 'undefined') {
                                                 alert('Another copy has barcode "' + barcode + '", defaulting to system-generated.');
                                                 barcode = '@@AUTO';
                                             }
+                                            if (!sound_setting) {
+                                                obj.data.no_sound = sound_setting; obj.data.stash('no_sound');
+                                            }
                                         }
                                         barcodes[item.id()] = barcode;
 
                                         // now call numbers
-                                        if (typeof call_numbers_by_issuance_id[item.issuance().id()] == 'undefined') {
+                                        if (typeof call_numbers_by_siss_and_sdist[item.issuance().id() + '@' + item.stream().distribution().id()] == 'undefined') {
                                             var default_cn = 'DEFAULT';
                                             // if they defined a *_call_number, honor it as the default
                                             var preset_cn_id = item.stream().distribution()[obj.mode + '_call_number']();
@@ -412,11 +419,10 @@ serial.manage_items.prototype = {
                                                 }
                                             } else {
                                                 // for now, let's default to the last created call number if there is one
-                                                // TODO: make this distribution specific
                                                 var acn_list = obj.network.request(
                                                         'open-ils.pcrud',
                                                         'open-ils.pcrud.search.acn',
-                                                        [ ses(), {"record" : obj.docid, "owning_lib" : obj.holding_lib, "deleted" : 'f' }, {"order_by" : {"acn" : "create_date DESC"}, "limit" : "1" } ]
+                                                        [ ses(), {"record" : obj.docid, "owning_lib" : item.stream().distribution().holding_lib(), "deleted" : 'f' }, {"order_by" : {"acn" : "create_date DESC"}, "limit" : "1" } ]
                                                 );
 
                                                 if (acn_list) {
@@ -433,10 +439,10 @@ serial.manage_items.prototype = {
                                                 call_number = 'DEFAULT'; //TODO: real default by setting
                                             }
                                             call_numbers[item.id()] = call_number;
-                                            call_numbers_by_issuance_id[item.issuance().id()] = call_number;
+                                            call_numbers_by_siss_and_sdist[item.issuance().id() + '@' + item.stream().distribution().id()] = call_number;
                                         } else {
-                                            // we have already seen this same issuance, so use the same call number
-                                            call_numbers[item.id()] = call_numbers_by_issuance_id[item.issuance().id()];
+                                            // we have already seen this same issuance and distribution combo, so use the same call number
+                                            call_numbers[item.id()] = call_numbers_by_siss_and_sdist[item.issuance().id() + '@' + item.stream().distribution().id()];
                                         }
 
                                         if (obj.current_sunit_id == -2) {
@@ -531,6 +537,7 @@ serial.manage_items.prototype = {
 
        'rebuild_current_sunit' : function(sdist_label, sdist_id, sstr_id) {
                var obj = this;
+        if (!obj.current_sunit_id) return; // current sunit is NONE
                try {
             var robj = obj.network.request(
                 'open-ils.pcrud',
@@ -553,7 +560,7 @@ serial.manage_items.prototype = {
             obj.current_sunit_id = sunit_id;
             obj.current_sunit_sdist_id = sdist_id;
             obj.current_sunit_sstr_id = sstr_id;
-            if (sunit_id < 0) {
+            if (sunit_id < 0  || sunit_id === '') {
                 $('serial_workarea_sunit_desc').firstChild.nodeValue = '**' + label + '**';
             } else {
                 $('serial_workarea_sunit_desc').firstChild.nodeValue = label;
@@ -653,7 +660,9 @@ serial.manage_items.prototype = {
             obj.mode = mode;
         }
 
-        if (mode == 'receive') {
+        obj.set_sdist_ids();
+
+        if (mode == 'receive' || mode == 'advanced_receive') {
             $('serial_workarea_mode_label').value = 'Recently Received';
             if ($('serial_manage_items_show_all').checked) {
                 obj.lists.main.sitem_retrieve_params = {};
@@ -664,8 +673,20 @@ serial.manage_items.prototype = {
 
             obj.lists.workarea.sitem_retrieve_params = {'date_received' : {"!=" : null}};
             obj.lists.workarea.sitem_extra_params ={'order_by' : {'sitem' : 'date_received DESC'}, 'limit' : 30};
+            if (mode == 'receive') {
+                $('serial_manage_items_context').value = $('serialStrings').getString('staff.serial.manage_items.subscriber.label') + ':';
+                $('cmd_set_other_sunit').setAttribute('disabled','true');
+                $('serial_items_recent_sunits').disabled = true;
+                obj.process_unit_selection($('serial_items_auto_per_item_menuitem'));
+                //obj.set_sunit(obj.current_sunit_id, label, sdist_id, sstr_id);
+            } else {
+                $('serial_manage_items_context').value = $('serialStrings').getString('staff.serial.manage_items.holder.label') + ':';
+                $('cmd_set_other_sunit').setAttribute('disabled','false');
+                $('serial_items_recent_sunits').disabled = false;
+            }    
         } else { // bind mode
             $('serial_workarea_mode_label').value = 'Bound Items in Current Working Unit';
+            $('serial_manage_items_context').value = $('serialStrings').getString('staff.serial.manage_items.holder.label') + ':';
             if ($('serial_manage_items_show_all').checked) {
                 obj.lists.main.sitem_retrieve_params = {};
             } else {
@@ -676,6 +697,8 @@ serial.manage_items.prototype = {
             obj.lists.workarea.sitem_retrieve_params = {}; // unit set dynamically in 'retrieve'
             obj.lists.workarea.sitem_extra_params ={'order_by' : {'sitem' : 'date_received DESC'}};
 
+            $('cmd_set_other_sunit').setAttribute('disabled','false');
+            $('serial_items_recent_sunits').disabled = false;
             // default to **NEW UNIT**
             // For now, keep the unit static.  TODO: Eventually, keep track of and store the last used unit value for both receive and bind separately
             // obj.set_sunit(-2, 'New Unit', '', '');
@@ -686,7 +709,7 @@ serial.manage_items.prototype = {
                var obj = this;
 
         JSAN.use('util.file'); var file = new util.file('serial_items_prefs.'+obj.data.server_unadorned);
-        util.widgets.save_attributes(file, { 'serial_item_lib_menu' : [ 'value' ], 'mode_receive' : [ 'selected' ], 'mode_bind' : [ 'selected' ], 'serial_manage_items_show_all' : [ 'checked' ] });
+        util.widgets.save_attributes(file, { 'serial_item_lib_menu' : [ 'value' ], 'mode_receive' : [ 'selected' ], 'mode_advanced_receive' : [ 'selected' ], 'mode_bind' : [ 'selected' ], 'serial_manage_items_show_all' : [ 'checked' ] });
     },
 
        'init_lists' : function() {
@@ -846,7 +869,22 @@ serial.manage_items.prototype = {
                obj.controller.view.sel_mark_items_missing.setAttribute('disabled','false');*/
 
                obj.retrieve_ids = list;
-       }
+       },
+
+    'process_unit_selection' : function(menuitem) {
+        var obj = this;
+
+        var label = menuitem.label;
+        var sunit_id = menuitem.getAttribute('sunit_id');
+        var sdist_id = menuitem.getAttribute('sdist_id');
+        var sstr_id = menuitem.getAttribute('sstr_id');
+        obj.set_sunit(sunit_id, label, sdist_id, sstr_id);
+        obj.save_sunit(sunit_id, label, sdist_id, sstr_id);
+        if (obj.mode == 'bind') {
+            obj.refresh_list('main');
+            obj.refresh_list('workarea');
+        }
+    }
 }
 
 function item_columns(modify,params) {
@@ -881,6 +919,14 @@ function item_columns(modify,params) {
             'hidden' : false,
             'persist' : 'hidden width ordinal',
             'render' : function(my) { return my.sitem.stream().distribution().label(); }
+        },        {
+            'id' : 'distribution_ou',
+            'label' : $('serialStrings').getString('staff.serial.manage_items.holder.label'),
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : false,
+            'persist' : 'hidden width ordinal',
+            'render' : function(my) { return data.hash.aou[ my.sitem.stream().distribution().holding_lib() ].shortname(); }
         },
         {
             'id' : 'stream_id',
index 97ae1f1..c5bedca 100644 (file)
@@ -47,8 +47,9 @@ vim:noet:sw=4:ts=4:
     </popupset>
     <tabpanel id="serial_manage_items" orient="vertical" flex="1">
         <hbox align="center">
+            <label id="serial_manage_items_context" style="font-weight: bold" value="&staff.serial.manage_items.context.label;"/>
             <hbox id="serial_item_lib_menu_box"/>
-            <label value="&staff.serial.manage_items.mode;" control="mode_receive"/><radiogroup id="serial_manage_items_mode" orient="horizontal"><radio id="mode_receive" label="&staff.serial.manage_items.receive.label;"/><radio id="mode_bind" label="&staff.serial.manage_items.bind.label;"/></radiogroup><checkbox id="serial_manage_items_show_all" label="&staff.serial.manage_items.show_all.label;" />
+            <label value="&staff.serial.manage_items.mode;" control="mode_receive"/><radiogroup id="serial_manage_items_mode" orient="horizontal"><radio id="mode_receive" label="&staff.serial.manage_items.receive.label;"/><radio id="mode_advanced_receive" label="&staff.serial.manage_items.advanced_receive.label;"/><radio id="mode_bind" label="&staff.serial.manage_items.bind.label;"/></radiogroup><checkbox id="serial_manage_items_show_all" label="&staff.serial.manage_items.show_all.label;" />
             <button id="refresh_button" label="&staff.cat.copy_browser.holdings_maintenance.refresh_button.label;" command="cmd_refresh_list" />
             <spacer flex="1"/>
             <menubar>
@@ -100,7 +101,8 @@ vim:noet:sw=4:ts=4:
                 <menu label="&staff.serial.manage_items.set_current_unit.label;" id="serial_items_current_sunit" sunit_id="-1" sunit_label="&staff.serial.manage_items.auto_per_item.label;" sdist_id="" sstr_id="">
                     <menupopup>
                         <menuitem command="cmd_set_sunit" label="&staff.serial.manage_items.new_unit.label;" sunit_id="-2" sdist_id="" sstr_id=""/>
-                        <menuitem command="cmd_set_sunit" label="&staff.serial.manage_items.auto_per_item.label;" sunit_id="-1" sdist_id="" sstr_id=""/>
+                        <menuitem id="serial_items_auto_per_item_menuitem" command="cmd_set_sunit" label="&staff.serial.manage_items.auto_per_item.label;" sunit_id="-1" sdist_id="" sstr_id=""/>
+                        <menuitem command="cmd_set_sunit" label="&staff.serial.manage_items.no_unit.label;" sunit_id="" sdist_id="" sstr_id=""/>
                         <menu label="&staff.serial.manage_items.recent.label;" id="serial_items_recent_sunits" sunit_json='[]'/>
                         <menuitem command="cmd_set_other_sunit" label="&staff.serial.manage_items.other_unit.label;"/>
                     </menupopup>
index 8919a6e..1dc83ac 100644 (file)
@@ -76,7 +76,7 @@ serial.sbsum_editor.prototype = {
             [
                 'textual_holdings',
                 { 
-                    input: 'c = function(v){ obj.apply("textual_holdings",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("size", 85); x.setAttribute("value",obj.editor_values.textual_holdings); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    input: 'c = function(v){ obj.apply("textual_holdings",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("multiline",true); x.setAttribute("cols", 80); x.setAttribute("value",obj.editor_values.textual_holdings); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
                     value_key: 'textual_holdings'
                 }
             ],
index 3131794..44f4627 100644 (file)
@@ -98,7 +98,7 @@ serial.scap_editor.prototype = {
             [
                 'pattern_code',
                 { 
-                    input: 'c = function(v){ obj.apply("pattern_code",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("value",obj.editor_values.pattern_code); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    input: 'c = function(v){ obj.apply("pattern_code",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("multiline",true); x.setAttribute("cols",40); x.setAttribute("value",obj.editor_values.pattern_code); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
                     value_key: 'pattern_code'
                 }
             ]
index 142899e..0045f46 100644 (file)
@@ -159,6 +159,15 @@ serial.sdist_editor.prototype = {
                 }
             ],
             [
+                'summary_method',
+                {
+                    render: 'obj.summary_methods[fm.summary_method()]',
+                    input: 'c = function(v){ obj.apply("summary_method",v); if (typeof post_c == "function") post_c(v); }; x = util.widgets.make_menulist( util.functional.map_object_to_list( obj.summary_methods, function(obj,i) { return [ obj[i], i ]; })); x.setAttribute("value",obj.editor_values.summary_method); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    value_key: 'summary_method',
+                    dropdown_key: 'fm.summary_method() == null ? null : fm.summary_method()'
+                }
+            ],
+            [
                 'unit_label_prefix',
                 {
                     input: 'c = function(v){ obj.apply("unit_label_prefix",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("value",obj.editor_values.unit_label_prefix); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
@@ -390,8 +399,14 @@ serial.sdist_editor.prototype = {
             obj.error.standard_unexpected_error_alert('get_act_list',E);
             return [];
         }
+    },
+    /******************************************************************************************************/
+    'summary_methods' : {
+        "add_to_sre" : $('serialStrings').getString('staff.serial.sdist_editor.add_to_sre.label'),
+        "merge_with_sre" : $('serialStrings').getString('staff.serial.sdist_editor.merge_with_sre.label'),
+        "use_sre_only" : $('serialStrings').getString('staff.serial.sdist_editor.use_sre_only.label'),
+        "use_sdist_only" : $('serialStrings').getString('staff.serial.sdist_editor.use_sdist_only.label'),
     }
-
 };
 
 dump('exiting serial/sdist_editor.js\n');
index 561e6bb..666c761 100644 (file)
@@ -137,7 +137,7 @@ serial.siss_editor.prototype = {
             [
                 'holding_code',
                 {
-                    input: 'c = function(v){ obj.apply("holding_code",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("value",obj.editor_values.holding_code); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    input: 'c = function(v){ obj.apply("holding_code",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("multiline",true); x.setAttribute("cols",40); x.setAttribute("value",obj.editor_values.holding_code); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
                     value_key: 'holding_code'
                 }
             ],
index 4c0e5c4..8c8a7c7 100644 (file)
@@ -76,7 +76,7 @@ serial.sisum_editor.prototype = {
             [
                 'textual_holdings',
                 { 
-                    input: 'c = function(v){ obj.apply("textual_holdings",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("size", 85); x.setAttribute("value",obj.editor_values.textual_holdings); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    input: 'c = function(v){ obj.apply("textual_holdings",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("multiline",true); x.setAttribute("cols", 80); x.setAttribute("value",obj.editor_values.textual_holdings); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
                     value_key: 'textual_holdings'
                 }
             ],
index c1a3d1f..9ab8efc 100644 (file)
@@ -76,7 +76,7 @@ serial.sssum_editor.prototype = {
             [
                 'textual_holdings',
                 { 
-                    input: 'c = function(v){ obj.apply("textual_holdings",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("size", 85); x.setAttribute("value",obj.editor_values.textual_holdings); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    input: 'c = function(v){ obj.apply("textual_holdings",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("multiline",true); x.setAttribute("cols", 80); x.setAttribute("value",obj.editor_values.textual_holdings); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
                     value_key: 'textual_holdings'
                 }
             ],
index 950d24f..151df8a 100644 (file)
@@ -8,3 +8,4 @@
 .copy_editor_field_changed { background: lightgreen; }
 .copy_editor_field_required { border: solid thin red; }
 
+hbox#batch_bar { background-color: gray; }
index a71d7a0..2b00bb2 100644 (file)
@@ -7,3 +7,23 @@
     urls['browser'] = '/opac/' + LOCALE + '/skin/mylib/xml/advanced.xml?nps=1';
 
 */
+
+// Debugging aids.  _dump_level = 4 enables all dump statements
+_dump_level = 4;
+var _dump_prefix = '0';
+try {
+    netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+    var prefs = Components.classes['@mozilla.org/preferences-service;1'].getService(Components.interfaces['nsIPrefBranch']);
+    if (!prefs.prefHasUserValue('oils.unique_id')) {
+        prefs.setIntPref('oils.unique_id',Number(_dump_prefix));
+    } else {
+        var temp = prefs.getIntPref('oils.unique_id') + 1;
+        prefs.setIntPref('oils.unique_id',temp);
+        _dump_prefix = String( temp );
+    }
+    dump(' _dump_prefix ' + _dump_prefix + ' = ' + location.href + '\n');
+} catch(E) {
+    dump('Error in custom.js trying to set oils.unique_id\n');
+}
+
+
index abf7676..b8ffe3b 100644 (file)
@@ -25,9 +25,29 @@ treechildren::-moz-tree-cell-text(selected,focus) {
 }
 */
 
-/*
-* { font-size-adjust: .5; }
-*/
+.ALL_FONTS_SMALLER * { font-size-adjust: .3 !important;}
+.ALL_FONTS_LARGER * { font-size-adjust: 1.1 !important; }
+.ALL_FONTS_XX_SMALL * { font-size: xx-small !important; }
+.ALL_FONTS_X_SMALL * { font-size: x-small !important; }
+.ALL_FONTS_SMALL * { font-size: small !important; }
+.ALL_FONTS_MEDIUM * { font-size: medium !important; }
+.ALL_FONTS_LARGE * { font-size: large !important; }
+.ALL_FONTS_X_LARGE * { font-size: x-large !important; }
+.ALL_FONTS_XX_LARGE * { font-size: xx-large !important; }
+.ALL_FONTS_5PT * { font-size: 5pt !important; }
+.ALL_FONTS_6PT * { font-size: 6pt !important; }
+.ALL_FONTS_7PT * { font-size: 7pt !important; }
+.ALL_FONTS_8PT * { font-size: 8pt !important; }
+.ALL_FONTS_9PT * { font-size: 9pt !important; }
+.ALL_FONTS_10PT * { font-size: 10pt !important; }
+.ALL_FONTS_11PT * { font-size: 11pt !important; }
+.ALL_FONTS_12PT * { font-size: 12pt !important; }
+.ALL_FONTS_13PT * { font-size: 13pt !important; }
+.ALL_FONTS_14PT * { font-size: 14pt !important; }
+.ALL_FONTS_15PT * { font-size: 15pt !important; }
+.ALL_FONTS_16PT * { font-size: 16pt !important; }
+.ALL_FONTS_17PT * { font-size: 17pt !important; }
+.ALL_FONTS_18PT * { font-size: 18pt !important; }
 
 .ALL_FONTS_SMALLER *|* { font-size-adjust: .3 !important;}
 .ALL_FONTS_LARGER *|* { font-size-adjust: 1.1 !important; }
@@ -58,6 +78,8 @@ treechildren::-moz-tree-cell-text(selected,focus) {
 *|textarea:focus { background-color: #DDFFDD; }
 *|input:focus { background-color: #DDFFDD; }
 
+button.sym {width:2em; min-width:1em; }
+
 .outline_me { -moz-outline: solid; }
 .clipboard_outline_me { -moz-outline: dotted thin gray; }
 
index f2b3164..847eedc 100644 (file)
@@ -3,7 +3,7 @@
 ; HM NIS Edit Wizard helper defines
 ; Old versions of makensis don't like this, moved to Makefile
 ;!define /file PRODUCT_VERSION "client/VERSION"
-!define PRODUCT_TAG "Trunk"
+!define PRODUCT_TAG "Master"
 !define PRODUCT_INSTALL_TAG "${PRODUCT_TAG}"
 !define UI_IMAGESET "beta"
 ;!define UI_IMAGESET "release"
 !define MUI_LANGDLL_REGISTRY_KEY "${PRODUCT_UNINST_KEY}"
 !define MUI_LANGDLL_REGISTRY_VALUENAME "NSIS:Language"
 
+; Make the welcome page a tad less verbose on the name
+; Note: The title bar will still be verbose (full product name + version + "Setup")
+!define MUI_WELCOMEPAGE_TITLE "Welcome to the Evergreen Staff Client ${PRODUCT_VERSION} Setup Wizard"
+
 ; Welcome page
 !insertmacro MUI_PAGE_WELCOME
 ; License page, if we have one
@@ -69,9 +73,7 @@ var ICONS_GROUP
 !insertmacro MUI_UNPAGE_INSTFILES
 
 ; Language files
-!insertmacro MUI_LANGUAGE "Czech"
 !insertmacro MUI_LANGUAGE "English"
-!insertmacro MUI_LANGUAGE "French"
 
 ; MUI end ------
 
@@ -114,9 +116,11 @@ Section "Staff Client" SECMAIN
   CreateShortCut "$DESKTOP\Evergreen Staff Client ${PRODUCT_TAG}.lnk" "$INSTDIR\evergreen.exe"
   
   ; External script for extra things.
+  !ifdef EXTRAS
   !define EXTERNAL_EXTRAS_SECMAIN
   !include /NONFATAL "extras.nsi"
   !undef EXTERNAL_EXTRAS_SECMAIN
+  !endif
 
   !insertmacro MUI_STARTMENU_WRITE_END
 
@@ -208,6 +212,7 @@ Section -Post
 SectionEnd
 
 ; Section descriptions
+!ifdef AUTOUPDATE | DEVELOPER | PERMACHINE
 !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
   !insertmacro MUI_DESCRIPTION_TEXT ${SECMAIN} "The Evergreen Staff Client with XULRunner, Required"
   !ifdef AUTOUPDATE
@@ -220,7 +225,7 @@ SectionEnd
   !insertmacro MUI_DESCRIPTION_TEXT ${SECPERMAC}  "Default registration and offline storage to per machine instead of per user"
   !endif
 !insertmacro MUI_FUNCTION_DESCRIPTION_END
-
+!endif
 
 Function un.onUninstSuccess
   HideWindow
@@ -265,9 +270,11 @@ Section Uninstall
   Delete "$SMPROGRAMS\$ICONS_GROUP\Evergreen Staff Client.lnk"
 
   ; External script for removing extra files before we wipe out the install directory
+  !ifdef EXTRAS
   !define EXTERNAL_EXTRAS_UNINSTALL
   !include /NONFATAL "extras.nsi"
   !undef EXTERNAL_EXTRAS_UNINSTALL
+  !endif
 
   RMDir "$SMPROGRAMS\$ICONS_GROUP"
   RMDir /r "$INSTDIR\updates"
diff --git a/README b/README
index 8644216..4ac7ab0 100644 (file)
--- a/README
+++ b/README
@@ -1,57 +1,86 @@
-README for Evergreen trunk
+README for Evergreen master
+========================
 
 Installing prerequisites:
-========================
+-------------------------
+
 Evergreen has a number of prerequisite packages that must be installed
 before you can successfully configure, compile, and install Evergreen.
 
-1. Begin by installing the most recent version of OpenSRF (1.6.2 or later).
-You can download OpenSRF releases from
-http://evergreen-ils.org/downloads
-
-2. On Debian and Ubuntu, the easiest way to install the rest of the
-prerequisites for Evergreen is to use the Makefile.install prerequisite
-installer.
-
-Issue the following commands as the root user to install prerequisites
-using the Makefile.install prerequisite installer, substituting
-"debian-etch", "debian-lenny", "fedora-14", "ubuntu-hardy", "ubuntu-lucid",
-"centos", or "rhel" for <osname> below:
-
+1. Begin by installing the most recent version of OpenSRF (2.0 or later).
+   You can download OpenSRF releases from http://evergreen-ils.org/downloads
+2. On many distributions, it is necessary to install Postgres 9 from external
+   repositories.
++
+  * On Debian Squeeze, add the following line to `/etc/apt/sources.list`:
++
+[source, bash]
+------------------------------------------------------------------------------
+deb http://backports.debian.org/debian-backports squeeze-backports main contrib
+------------------------------------------------------------------------------
++
+  * On Ubuntu Lucid, add the following line to `/etc/apt/sources.list`:
++
+[source, bash]
+------------------------------------------------------------------------------
+deb http://archive.ubuntu.com/ubuntu lucid-backports main universe multiverse restricted
+------------------------------------------------------------------------------
++
+  * On Fedora 14, follow the http://yum.pgrpms.org/howtoyum.php[instructions
+    in the Yum HOWTO] to enable the PostgreSQL RPM Building Project yum
+    repository.
++
+3. On Debian and Ubuntu, the easiest way to install the rest of the
+   prerequisites for Evergreen is to use the Makefile.install prerequisite
+   installer.
+4. Issue the following commands as the root user to install prerequisites
+   using the Makefile.install prerequisite installer, substituting
+   `debian-squeeze`, `fedora-14`, `ubuntu-lucid`, `centos`, or `rhel` for
+   <osname> below:
++
+[source, bash]
+------------------------------------------------------------------------------
 make -f Open-ILS/src/extras/Makefile.install <osname>
+------------------------------------------------------------------------------
 
-Note: "centos" and "rhel" are less tested than the debian, fedora,
-and ubuntu options.  Your patches and suggestions for improvement are
+Note: `centos` and `rhel` are less tested than the `debian`, `fedora`,
+and `ubuntu` options.  Your patches and suggestions for improvement are
 welcome!
 
 Configuration and compilation instructions:
-==========================================
+-------------------------------------------
 
-For the time being, we are still installing everything in the /openils/
+For the time being, we are still installing everything in the `/openils/`
 directory. If you are working with a version of Evergreen taken directly
-from the Subversion repository, rather than a packaged version of Evergreen,
-first see "Developer instructions" below.
+from the Git repository, rather than a packaged version of Evergreen,
+first see `Developer instructions` below.
 
 Otherwise, issue the following commands to configure and build Evergreen:
 
+[source, bash]
+------------------------------------------------------------------------------
 ./configure --prefix=/openils --sysconfdir=/openils/conf
 make
+------------------------------------------------------------------------------
 
 Installation instructions:
-=========================
+--------------------------
 
 Once you have configured and compiled Evergreen, issue the following
 command as the root user to install Evergreen:
 
+[source, bash]
+------------------------------------------------------------------------------
 make STAFF_CLIENT_STAMP_ID=rel_trunk install
+------------------------------------------------------------------------------
 
 This will install Evergreen, including example configuration files in
-/openils/conf/ that you can use as templates for your own configuration files.
-The STAFF_CLIENT_STAMP_ID variable stamps the server-side and client-side files
+`/openils/conf/` that you can use as templates for your own configuration files.
+The `STAFF_CLIENT_STAMP_ID` variable stamps the server-side and client-side files
 for the staff client to ensure that they match.
 
 Install Dojo Toolkit:
-====================
+---------------------
 
 Evergreen uses the Dojo Toolkit to support its Web and staff client interfaces.
 
@@ -62,83 +91,100 @@ Issue the following commands as the root user to fetch, extract, and copy the
 files into the correct directory, adjusting the version number to match the
 version of the Dojo Toolkit that you downloaded:
 
+[source, bash]
+------------------------------------------------------------------------------
 wget http://download.dojotoolkit.org/release-1.3.3/dojo-release-1.3.3.tar.gz
 tar -C /openils/var/web/js -xzf dojo-release-1.3.3.tar.gz
 cp -r /openils/var/web/js/dojo-release-1.3.3/* /openils/var/web/js/dojo/.
+------------------------------------------------------------------------------
 
 Create the oils_web.xml configuration file:
-==========================================
+-------------------------------------------
 Many administration interfaces, such as acquisitions, bookings, and various
 configuration screens, depend on the correct configuration of HTML templates.
 Copying the sample configuration file into place should work in most cases:
 
+[source, bash]
+------------------------------------------------------------------------------
 cp /openils/conf/oils_web.xml.example /openils/conf/oils_web.xml
+------------------------------------------------------------------------------
 
 Change ownership of the Evergreen files:
-=======================================
+----------------------------------------
 
-All files in the /openils/ directory and subdirectories must be owned by the
-"opensrf" user. Issue the following command as the root user to change the
+All files in the `/openils/` directory and subdirectories must be owned by the
+`opensrf` user. Issue the following command as the root user to change the
 ownership on the files:
 
+[source, bash]
+------------------------------------------------------------------------------
 chown -R opensrf:opensrf /openils
+------------------------------------------------------------------------------
 
 Configure the Apache Web server:
-===============================
+--------------------------------
 
-Use the example configuration files in Open-ILS/examples/apache/ to configure
+Use the example configuration files in `Open-ILS/examples/apache/` to configure
 your Web server for the Evergreen catalog, staff client, Web services, and
 administration interfaces.
 
 Configure OpenSRF for the Evergreen application:
-===============================================
+------------------------------------------------
 
-There are a number of example OpenSRF configuration files in /openils/conf/ that
-you can use as a template for your Evergreen installation.
+There are a number of example OpenSRF configuration files in `/openils/conf/`
+that you can use as a template for your Evergreen installation.
 
+[source, bash]
+------------------------------------------------------------------------------
 cp /openils/conf/opensrf_core.xml.example /openils/conf/opensrf_core.xml
 cp /openils/conf/opensrf.xml.example /openils/conf/opensrf.xml
+------------------------------------------------------------------------------
 
 When you installed OpenSRF, you will have created four Jabber users on two
-separate domains and edited the opensrf_core.xml file accordingly. Please
+separate domains and edited the `opensrf_core.xml` file accordingly. Please
 refer back to the OpenSRF README and edit the Evergreen version of the
-opensrf_core.xml file using the same Jabber users and domains as you used
+`opensrf_core.xml` file using the same Jabber users and domains as you used
 while installing and testing OpenSRF.
 
-eg_db_config.pl, described in the following section, will set the database
-connection information in opensrf.xml for you.
+`eg_db_config.pl`, described in the following section, will set the database
+connection information in `opensrf.xml` for you.
 
 Creating the Evergreen database:
-===============================
+--------------------------------
 
-PostgreSQL 9.0 will be installed on your system by the Makefile.install
+PostgreSQL 9.0 will be installed on your system by the `Makefile.install`
 prerequisite installer if packages are available for your distribution, or
 you will have to compile PostgreSQL 9.0 from source and install it (which
 is beyond the scope of this document).
 
 Once the PostgreSQL database server has been installed, you will need to
 create the database and add the appropriate languages and extensions to
-support Evergreen. Issue the following commands as the "postgres" user to set
-up a database called "evergreen". Note that the location of the PostgreSQL
-"contrib" packages may vary depending on your distribution. In the following
+support Evergreen. Issue the following commands as the `postgres` user to set
+up a database called `evergreen`. Note that the location of the PostgreSQL
+`contrib` packages may vary depending on your distribution. In the following
 commands, we assume that you are working with PostgreSQL 9.0 on a Debian-based
 system:
 
+[source, bash]
+------------------------------------------------------------------------------
 createdb --template template0 --lc-ctype=C --lc-collate=C --encoding UNICODE evergreen
 createlang plperl evergreen
 createlang plperlu evergreen
-createlang plpgsql evergreen
 psql -f /usr/share/postgresql/9.0/contrib/tablefunc.sql -d evergreen
 psql -f /usr/share/postgresql/9.0/contrib/tsearch2.sql -d evergreen
 psql -f /usr/share/postgresql/9.0/contrib/pgxml.sql -d evergreen
 psql -f /usr/share/postgresql/9.0/contrib/hstore.sql -d evergreen
+------------------------------------------------------------------------------
 
 Once you have created the Evergreen database, you need to create a PostgreSQL
-user to access the database. Issue the following command as the "postgres"
-user to create a new PostgreSQL user named "evergreen". When prompted, enter
-the new user's password and answer "yes" to make the new role a superuser:
+user to access the database. Issue the following command as the `postgres`
+user to create a new PostgreSQL user named `evergreen`. When prompted, enter
+the new user's password and answer `yes` to make the new role a superuser:
 
+[source, bash]
+------------------------------------------------------------------------------
 createuser -P evergreen
+------------------------------------------------------------------------------
 
 Once you have created the Evergreen database, you also need to create the
 database schema and configure your configuration files to point at the
@@ -148,23 +194,26 @@ with the appropriate values for your PostgreSQL database, and <admin-user> and
 <admin-pass> with the values you want for the default Evergreen administrator
 account:
 
+[source, bash]
+------------------------------------------------------------------------------
 perl Open-ILS/src/support-scripts/eg_db_config.pl --update-config \
        --service all --create-schema --create-offline \
        --user <user> --password <password> --hostname <hostname> --port <port> \
        --database <dbname> --admin-user <admin-user> --admin-pass <admin-pass>
+------------------------------------------------------------------------------
 
 This will create the database schema and configure all of the services in
-your /openils/conf/opensrf.xml configuration file to point to that database.
+your `/openils/conf/opensrf.xml` configuration file to point to that database.
 It also creates the configuration files required by the Evergreen cgi-bin
 administration scripts, and set the user name and password for the default
 Evergreen administrator account to your requested values.
 
 Developer instructions:
-======================
+-----------------------
 
-Developers working directly with the source code from the Subversion
+Developers working directly with the source code from the Git
 repository will also need to install some extra packages and perform
-one more step before they can proceed with the "./configure" step.
+one more step before they can proceed with the `./configure` step.
 
 Install the following packages:
   * autoconf
@@ -174,19 +223,25 @@ Install the following packages:
 Run the following command in the source directory to generate the configure
 script and Makefiles:
 
-./autogen.sh 
+[source, bash]
+------------------------------------------------------------------------------
+./autogen.sh
+------------------------------------------------------------------------------
 
-After running 'make install', developers also need to install the Dojo Toolkit
+After running `make install`, developers also need to install the Dojo Toolkit
 set of JavaScript libraries. The appropriate version of Dojo is included
 in Evergreen release tarballs; developers should install the Dojo 1.3.3
 version of Dojo as follows:
 
+[source, bash]
+------------------------------------------------------------------------------
 wget http://download.dojotoolkit.org/release-1.3.3/dojo-release-1.3.3.tar.gz
 tar xzf dojo-release-1.3.3.tar.gz
 cp -r dojo-release-1.3.3/* /openils/var/web/js/dojo/.
+------------------------------------------------------------------------------
 
 Getting help:
-============
+-------------
 
 Need help installing or using Evergreen? Join the mailing lists at
 http://evergreen-ils.org/listserv.php or contact us on the Freenode
index f2fc92d..b52a9b2 100644 (file)
@@ -3,7 +3,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: Evergreen 1.4\n"
 "Report-Msgid-Bugs-To: open-ils-dev@list.georgialibraries.org\n"
-"POT-Creation-Date: 2011-04-18 21:45:31-0400\n"
+"POT-Creation-Date: 2011-04-23 01:33:28-0400\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -11,46 +11,130 @@ msgstr ""
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8-bit\n"
 
+#: register.js:USER_SETTINGS
+msgid "User Settings"
+msgstr ""
+
+#: register.js:DUPE_PATRON_EMAIL
+msgid "Found ${0} patron(s) with the same email address"
+msgstr ""
+
+#: register.js:ADDRESS_BILLING
+msgid "Billing"
+msgstr ""
+
+#: register.js:ADDRESS_OWNED
+msgid "This address is owned by another user: "
+msgstr ""
+
+#: register.js:REPLACED_ADDRESS
+msgid "<div>Replaces address <b>${0}</b><br/> ${1} ${2}<br/> ${3}, ${4} ${5}</div>"
+msgstr ""
+
+#: register.js:SHOW_REQUIRED
+msgid "Show Only Required Fields"
+msgstr ""
+
 #: register.js:DUPE_PATRON_ADDR
 msgid "Found ${0} patron(s) with the same address"
 msgstr ""
 
-#: register.js:INVALID_FORM
-msgid "Form is invalid.  Please edit and try again."
+#: register.js:SAVE
+msgid "Save"
 msgstr ""
 
 #: register.js:DUPE_PATRON_IDENT
 msgid "Found ${0} patron(s) with the same identification"
 msgstr ""
 
-#: register.js:DELETE_ADDRESS
-msgid "Delete address ${0}?"
+#: register.js:ADDRESS_MAILING
+msgid "Mailing"
 msgstr ""
 
-#: register.js:DUPE_PATRON_EMAIL
-msgid "Found ${0} patron(s) with the same email address"
-msgstr ""
-
-#: register.js:DUPE_PATRON_NAME
-msgid "Found ${0} patron(s) with the same name"
+#: register.js:REPLACE_BARCODE
+msgid "Replace Barcode"
 msgstr ""
 
 #: register.js:NEED_ADDRESS
 msgid "An address is required during registration."
 msgstr ""
 
-#: register.js:EXAMPLE
-msgid "Example: "
+#: register.js:PARENT_OR_GUARDIAN
+msgid "Parent/Guardian"
+msgstr ""
+
+#: register.js:ADDRESS_HEADER
+msgid "Address"
 msgstr ""
 
 #: register.js:DUPE_PATRON_PHONE
 msgid "Found ${0} patron(s) with the same phone number"
 msgstr ""
 
-#: register.js:REPLACED_ADDRESS
-msgid "<div>Replaces address <b>${0}</b><br/> ${1} ${2}<br/> ${3}, ${4} ${5}</div>"
+#: register.js:INVALID_FORM
+msgid "Form is invalid.  Please edit and try again."
+msgstr ""
+
+#: register.js:BARCODE_IN_USE
+msgid "Barcode is already in use"
+msgstr ""
+
+#: register.js:EXAMPLE
+msgid "Example: "
+msgstr ""
+
+#: register.js:SHOW_SUGGESTED
+msgid "Show Suggested Fields"
+msgstr ""
+
+#: register.js:ADDRESS_PENDING
+msgid "This is a pending address: "
+msgstr ""
+
+#: register.js:DUPE_USERNAME
+msgid "Username is already in use"
+msgstr ""
+
+#: register.js:DUPE_PATRON_NAME
+msgid "Found ${0} patron(s) with the same name"
 msgstr ""
 
 #: register.js:DEFAULT_ADDRESS_TYPE
 msgid "MAILING"
 msgstr ""
+
+#: register.js:SHOW_ALL
+msgid "Show All Fields"
+msgstr ""
+
+#: register.js:ADDRESS_APPROVE
+msgid "Approve Address"
+msgstr ""
+
+#: register.js:VERIFY_PASSWORD
+msgid "Verify Password"
+msgstr ""
+
+#: register.js:SAVE_CLONE
+msgid "Save &amp; Clone"
+msgstr ""
+
+#: register.js:STAT_CATS
+msgid "Statistical Categories"
+msgstr ""
+
+#: register.js:ADDRESS_NEW
+msgid "New Address"
+msgstr ""
+
+#: register.js:RESET_PASSWORD
+msgid "Reset Password"
+msgstr ""
+
+#: register.js:SEE_ALL
+msgid "See All"
+msgstr ""
+
+#: register.js:DELETE_ADDRESS
+msgid "Delete address ${0}?"
+msgstr ""
index 8b275c1..75f1f65 100755 (executable)
@@ -5,19 +5,14 @@
 # 
 # Based on initial version by Bill Erickson.
 
-function svn_or_git {
+function fetch_changes {
     echo -en "###########\nUpdating source directory:" `pwd` "\n";
     if [ -d "./.git" ]; then
-        if [ -d "./.git/svn/trunk" ]; then
-            git svn fetch;
-            # git svn rebase origin || die_msg "git svn rebase origin failed";
-        else
-            git fetch;
-            # git rebase origin || die_msg "git rebase origin failed";
-        fi
+        git fetch;
+        # git rebase origin || die_msg "git rebase origin failed";
     else
-        echo "Remember to run svn update as needed";
-        # svn update || die_msg "svn update failed";
+        echo "You don't appear to be using Git yet, please fix that"
+        exit 1;
     fi
 }
 
@@ -60,7 +55,7 @@ and error-prone tasks associated with an upgrade for a developer.
 Considerations:
  * Run as opensrf user
  * opensrf needs sudo 
- * Assumes opensrf has OpenILS and OpenSRF repositories as svn or git-svn 
+ * Assumes opensrf has OpenILS and OpenSRF repositories as Git
    checkouts and both have been configured (as in ./configure) 
   
 END_OF_USAGE
@@ -110,7 +105,7 @@ JSDIR="$INSTALL/lib/javascript";    # only used for FULL install
 [ ! -d "$OSRF"    ]   && die_msg "OpenSRF Source Directory '$OSRF' does not exist!";
 which sudo >/dev/null || die_msg "sudo not installed (or in PATH)";
 
-[ -d "${ILS}/.svn" ] || [ -d "${ILS}/.git" ] || [ -d ${ILS}/.bzr ] || die_msg "Evergreen Source Directory '$ILS' is not a SVN, bzr or git repo";
+[ -d "${ILS}/.git" ] || [ -d ${ILS}/.bzr ] || die_msg "Evergreen Source Directory '$ILS' is not a SVN, bzr or git repo";
 
 if [ ! -z "$OPT_TEST" ] ; then
     feedback;
@@ -140,8 +135,8 @@ $INSTALL/bin/osrf_ctl.sh -l -a stop_all;
 # OpenSRF perl directory is not shared.  update the drone
 # ssh 10.5.0.202 "./update_osrf_perl.sh";
 
-cd $OSRF; svn_or_git;
-cd $ILS;  svn_or_git;
+cd $OSRF; fetch_changes;
+cd $ILS;  fetch_changes;
 
 if [ -n "$OPT_CLEAN" ]; then
     cd $OSRF && make clean;
diff --git a/build/tools/update_git_svn.sh b/build/tools/update_git_svn.sh
deleted file mode 100755 (executable)
index ac50e71..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/bin/bash
-#
-# Author: Joe Atzberger
-#
-# This script will update your git-svn repository from the 
-# SVN source repo and push to a github remote (if one exists).
-#
-# The design is (somewhat) suitable for cronjob because it:
-#   ~ only updates the local "master" branch
-#   ~ dies if it cannot switch to "master"
-#   ~ switches back to whatever branch was current initially
-#
-# However, it will fail if you cannot switch branches, (i.e. 
-# have a lot of uncommited changes).  
-#
-# WARNING: you should NOT run this in crontab on a repo you
-# are actively developing since switching branches (even
-# momentarily) in the middle of editing or runtime could
-# seriously confuse any developer.  Instead, just run it 
-# manually as needed.
-#
-# Workflow might look like:
-#   git checkout -b my_feature
-#   [ edit, edit, edit ]
-#   git commit -a
-#   ./build/tools/update_git_svn.sh
-#   git rebase master
-#   git push github my_feature
-#
-
-function die_msg {
-    echo "ERROR at $1" >&2;
-    exit;
-}
-function parse_git_branch {
-    ref=$(git-symbolic-ref HEAD 2> /dev/null) || return;
-    ref=${ref#refs/heads/};
-    # echo "REF2: $ref";
-}
-
-
-parse_git_branch;
-BRANCH=$ref;
-
-echo "Current branch: $BRANCH";
-
-git svn fetch  || die_msg 'git svn fetch';
-# git status     || die_msg 'git status';
-git checkout master  || die_msg 'git checkout master';
-
-MESSAGE='';
-git svn rebase  || MESSAGE="ERROR at git svn rebase;  ";
-git checkout $BRANCH  || die_msg "${MESSAGE}git checkout $BRANCH";
-git push github master;
-
index 8131258..dd0b098 100644 (file)
@@ -378,6 +378,7 @@ AC_CONFIG_FILES([Makefile
          Open-ILS/src/extras/import/marc2bre.pl
          Open-ILS/src/extras/import/marc2sre.pl
          Open-ILS/src/extras/import/parallel_pg_loader.pl
+         Open-ILS/src/support-scripts/authority_control_fields.pl
          Open-ILS/src/support-scripts/marc_export
          Open-ILS/src/perlmods/Makefile
          Open-ILS/src/perlmods/lib/OpenILS/Utils/Cronscript.pm],
@@ -388,6 +389,7 @@ AC_CONFIG_FILES([Makefile
             if test -e "./Open-ILS/src/extras/import/marc2bre.pl"; then chmod 755 Open-ILS/src/extras/import/marc2bre.pl; fi;
             if test -e "./Open-ILS/src/extras/import/marc2sre.pl"; then chmod 755 Open-ILS/src/extras/import/marc2sre.pl; fi;
             if test -e "./Open-ILS/src/extras/import/parallel_pg_loader.pl"; then chmod 755 Open-ILS/src/extras/import/parallel_pg_loader.pl; fi;
+            if test -e "./Open-ILS/src/support-scripts/authority_control_fields.pl"; then chmod 755 Open-ILS/src/support-scripts/authority_control_fields.pl; fi;
             if test -e "./Open-ILS/src/support-scripts/marc_export"; then chmod 755 Open-ILS/src/support-scripts/marc_export; fi;
         ])
 AC_OUTPUT