[open-ils-commits] r19883 - in trunk/Open-ILS: examples src/perlmods/lib/OpenILS src/perlmods/lib/OpenILS/Application src/perlmods/lib/OpenILS/Application/Cat src/perlmods/lib/OpenILS/Application/Circ src/perlmods/lib/OpenILS/Application/Search src/perlmods/lib/OpenILS/Application/Storage src/perlmods/lib/OpenILS/Application/Storage/CDBI src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg src/perlmods/lib/OpenILS/Application/Storage/Publisher src/sql/Pg src/sql/Pg/upgrade web/js/ui/default/acq/common web/js/ui/default/conify/global/config web/opac/locale/en-US web/opac/skin/default/js web/opac/skin/default/xml/common web/opac/skin/default/xml/myopac web/opac/skin/default/xml/rdetail web/templates/default/conify/global web/templates/default/conify/global/biblio web/templates/default/conify/global/config xul/staff_client/chrome/content/OpenILS xul/staff_client/chrome/content/cat xul/staff_client/chrome/content/main xul/staff_client/chrome/content/util xul/staff_client/chrome/locale/en-US xul/staff_client/server/cat xul/staff_client/server/circ xul/staff_client/server/locale/en-US xul/staff_client/server/patron (miker)

svn at svn.open-ils.org svn at svn.open-ils.org
Tue Mar 29 11:24:47 EDT 2011


Author: miker
Date: 2011-03-29 11:24:44 -0400 (Tue, 29 Mar 2011)
New Revision: 19883

Added:
   trunk/Open-ILS/src/sql/Pg/upgrade/0504.schema.parts_and_cnaffix.sql
   trunk/Open-ILS/web/js/ui/default/conify/global/config/acn_prefix.js
   trunk/Open-ILS/web/js/ui/default/conify/global/config/acn_suffix.js
   trunk/Open-ILS/web/templates/default/conify/global/biblio/
   trunk/Open-ILS/web/templates/default/conify/global/biblio/monograph_part.tt2
   trunk/Open-ILS/web/templates/default/conify/global/config/acn_prefix.tt2
   trunk/Open-ILS/web/templates/default/conify/global/config/acn_suffix.tt2
   trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_editor.js
   trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_editor.xul
   trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_editor_horiz.xul
   trunk/Open-ILS/xul/staff_client/server/cat/volume_editor.js
Modified:
   trunk/Open-ILS/examples/fm_IDL.xml
   trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
   trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat.pm
   trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
   trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm
   trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
   trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
   trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Serial.pm
   trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI.pm
   trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/asset.pm
   trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/biblio.pm
   trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/dbi.pm
   trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
   trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/biblio.pm
   trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/SuperCat.pm
   trunk/Open-ILS/src/perlmods/lib/OpenILS/Const.pm
   trunk/Open-ILS/src/sql/Pg/002.schema.config.sql
   trunk/Open-ILS/src/sql/Pg/010.schema.biblio.sql
   trunk/Open-ILS/src/sql/Pg/040.schema.asset.sql
   trunk/Open-ILS/src/sql/Pg/950.data.seed-values.sql
   trunk/Open-ILS/src/sql/Pg/990.schema.unapi.sql
   trunk/Open-ILS/web/js/ui/default/acq/common/li_table.js
   trunk/Open-ILS/web/opac/locale/en-US/lang.dtd
   trunk/Open-ILS/web/opac/locale/en-US/opac.dtd
   trunk/Open-ILS/web/opac/skin/default/js/copy_details.js
   trunk/Open-ILS/web/opac/skin/default/js/holds.js
   trunk/Open-ILS/web/opac/skin/default/js/myopac.js
   trunk/Open-ILS/web/opac/skin/default/js/rresult.js
   trunk/Open-ILS/web/opac/skin/default/xml/common/holds.xml
   trunk/Open-ILS/web/opac/skin/default/xml/myopac/myopac_holds.xml
   trunk/Open-ILS/web/opac/skin/default/xml/rdetail/rdetail_cn_details.xml
   trunk/Open-ILS/xul/staff_client/chrome/content/OpenILS/data.js
   trunk/Open-ILS/xul/staff_client/chrome/content/OpenILS/global_util.js
   trunk/Open-ILS/xul/staff_client/chrome/content/cat/opac.js
   trunk/Open-ILS/xul/staff_client/chrome/content/cat/opac.xul
   trunk/Open-ILS/xul/staff_client/chrome/content/main/constants.js
   trunk/Open-ILS/xul/staff_client/chrome/content/main/menu.js
   trunk/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
   trunk/Open-ILS/xul/staff_client/chrome/content/util/browser.js
   trunk/Open-ILS/xul/staff_client/chrome/content/util/list.js
   trunk/Open-ILS/xul/staff_client/chrome/content/util/widgets.js
   trunk/Open-ILS/xul/staff_client/chrome/locale/en-US/offline.properties
   trunk/Open-ILS/xul/staff_client/server/cat/bib_brief.js
   trunk/Open-ILS/xul/staff_client/server/cat/bib_brief.xul
   trunk/Open-ILS/xul/staff_client/server/cat/copy_browser.js
   trunk/Open-ILS/xul/staff_client/server/cat/copy_browser.xul
   trunk/Open-ILS/xul/staff_client/server/cat/copy_editor.js
   trunk/Open-ILS/xul/staff_client/server/cat/copy_editor.xul
   trunk/Open-ILS/xul/staff_client/server/cat/spine_labels.js
   trunk/Open-ILS/xul/staff_client/server/cat/util.js
   trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_creator.js
   trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_creator.xul
   trunk/Open-ILS/xul/staff_client/server/cat/volume_editor.xul
   trunk/Open-ILS/xul/staff_client/server/circ/copy_status.js
   trunk/Open-ILS/xul/staff_client/server/circ/util.js
   trunk/Open-ILS/xul/staff_client/server/locale/en-US/cat.properties
   trunk/Open-ILS/xul/staff_client/server/locale/en-US/circ.properties
   trunk/Open-ILS/xul/staff_client/server/locale/en-US/common.properties
   trunk/Open-ILS/xul/staff_client/server/patron/util.js
Log:
Monograph Parts; Unified vol/copy wizard; Call Number affixes; Instant Detail

 * Monograph Bibliographic Parts - One MARC record should be able to support
   all volumes associated with the title, subdivide records in multiple ways,
   and the ability to support holds on individual Parts.

 * Unified Volume/Copy Wizard - The ability to enter call number, barcode
   number, and all copy information on the same screen.  Also, the ability
   to include some information associated with the call number, such as
   classification scheme, prefix and suffix, in the item template feature
   of the Unified Volume/Copy Wizard.

 * Call Number Affixes - Delimiting the call number so that the prefix and
   suffix reside in separate fields.  This prefix and suffix should display
   in the OPAC as part of the call number and should also be available for
   use in spine labels.

 * Instant Detail for One Record Hit List - When searching for records in
   the staff client, the ability to immediately land on the bibliographic
   record instead of the search results screen when there is only on matching
   search result in the system.  This enhancement does not change the way
   search results are returned in the public catalog.

These features were sponsored by the Massachusetts Library Network Cooperative.

Modified: trunk/Open-ILS/examples/fm_IDL.xml
===================================================================
--- trunk/Open-ILS/examples/fm_IDL.xml	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/examples/fm_IDL.xml	2011-03-29 15:24:44 UTC (rev 19883)
@@ -1814,14 +1814,57 @@
             </actions>
         </permacrud>
 	</class>
-    <class id="acnc" controller="open-ils.cstore" oils_obj:fieldmapper="asset::call_number_class" oils_persist:tablename="asset.call_number_class" reporter:label="Call number classification scheme">
+    <class id="acnc" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::call_number_class" oils_persist:tablename="asset.call_number_class" reporter:label="Call number classification scheme">
         <fields oils_persist:primary="id" oils_persist:sequence="asset.call_number_class_id_seq">
             <field reporter:label="Call number class ID" name="id" reporter_datatype="id"/>
             <field reporter:label="Name" name="name" reporter:datatype="text"/>
             <field reporter:label="Normalizer function" name="normalizer" reporter:datatype="text"/>
             <field reporter:label="Call number fields" name="field" reporter:datatype="text"/>
         </fields>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <retrieve/>
+            </actions>
+        </permacrud>
     </class>
+	<class id="acns" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::call_number_suffix" oils_persist:tablename="asset.call_number_suffix" reporter:label="Call Number/Volume Suffix">
+		<fields oils_persist:primary="id" oils_persist:sequence="asset.call_number_suffix_id_seq">
+			<field reporter:label="ID" name="id" reporter:datatype="id" />
+			<field reporter:label="Label" name="label" reporter:datatype="text"/>
+			<field reporter:label="Label Sort Key" name="label_sortkey" reporter:datatype="text"/>
+			<field reporter:label="Owning Library" name="owning_lib"  reporter:datatype="org_unit"/>
+		</fields>
+		<links>
+			<link field="owning_lib" reltype="has_a" key="id" map="" class="aou"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="CREATE_VOLUME_SUFFIX" context_field="owning_lib"/>
+                <retrieve/>
+                <update permission="UPDATE_VOLUME_SUFFIX" context_field="owning_lib"/>
+                <delete permission="DELETE_VOLUME_SUFFIX" context_field="owning_lib"/>
+            </actions>
+        </permacrud>
+	</class>
+	<class id="acnp" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::call_number_prefix" oils_persist:tablename="asset.call_number_prefix" reporter:label="Call Number/Volume Prefix">
+		<fields oils_persist:primary="id" oils_persist:sequence="asset.call_number_prefix_id_seq">
+			<field reporter:label="ID" name="id" reporter:datatype="id" />
+			<field reporter:label="Label" name="label" reporter:datatype="text"/>
+			<field reporter:label="Label Sort Key" name="label_sortkey" reporter:datatype="text"/>
+			<field reporter:label="Owning Library" name="owning_lib"  reporter:datatype="org_unit"/>
+		</fields>
+		<links>
+			<link field="owning_lib" reltype="has_a" key="id" map="" class="aou"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="CREATE_VOLUME_PREFIX" context_field="owning_lib"/>
+                <retrieve/>
+                <update permission="UPDATE_VOLUME_PREFIX" context_field="owning_lib"/>
+                <delete permission="DELETE_VOLUME_PREFIX" context_field="owning_lib"/>
+            </actions>
+        </permacrud>
+	</class>
 	<class id="acn" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::call_number" oils_persist:tablename="asset.call_number" reporter:label="Call Number/Volume">
 		<fields oils_persist:primary="id" oils_persist:sequence="asset.call_number_id_seq">
 			<field reporter:label="Copies" name="copies" oils_persist:virtual="true" reporter:datatype="link"/>
@@ -1839,6 +1882,8 @@
 			<field reporter:label="URIs" name="uris" oils_persist:virtual="true" reporter:datatype="link"/>
 			<field reporter:label="Sort Key" name="label_sortkey" reporter:datatype="text"/>
 			<field reporter:label="Classification Scheme" name="label_class" reporter:datatype="link"/>
+			<field reporter:label="Prefix" name="prefix" reporter:datatype="link"/>
+			<field reporter:label="Suffix" name="suffix" reporter:datatype="link"/>
 		</fields>
 		<links>
 			<link field="editor" reltype="has_a" key="id" map="" class="au"/>
@@ -1850,6 +1895,8 @@
 			<link field="uris" reltype="has_many" key="call_number" map="uri" class="auricnm"/>
 			<link field="uri_maps" reltype="has_many" key="call_number" map="" class="auricnm"/>
 			<link field="label_class" reltype="has_a" key="id" map="" class="acnc"/>
+			<link field="prefix" reltype="has_a" key="id" map="" class="acnp"/>
+			<link field="suffix" reltype="has_a" key="id" map="" class="acns"/>
 		</links>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
             <actions>
@@ -2027,6 +2074,44 @@
             </actions>
         </permacrud>
 	</class>
+	<class id="bmp" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="biblio::monograph_part" oils_persist:tablename="biblio.monograph_part" reporter:label="Monograph Parts" oils_persist:field_safe="true">
+		<fields oils_persist:primary="id" oils_persist:sequence="biblio.monograph_part_id_seq">
+			<field name="id" reporter:datatype="id" />
+			<field name="record" reporter:datatype="link"/>
+			<field name="label" reporter:datatype="text"/>
+			<field name="label_sortkey" reporter:datatype="text"/>
+		</fields>
+		<links>
+			<link field="record" reltype="has_a" key="id" map="" class="bre"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="CREATE_MONOGRAPH_PART" global_required="true"/>
+                <retrieve/>
+                <update permission="UPDATE_MONOGRAPH_PART" global_required="true"/>
+                <delete permission="DELETE_MONOGRAPH_PART" global_required="true"/>
+            </actions>
+        </permacrud>
+	</class>
+	<class id="acpm" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::copy_part_map" oils_persist:tablename="asset.copy_part_map" reporter:label="Copy Monograph Part Map">
+		<fields oils_persist:primary="id" oils_persist:sequence="asset.copy_part_map_id_seq">
+			<field name="id" reporter:datatype="id" />
+			<field name="target_copy" reporter:datatype="link" />
+			<field name="part" reporter:datatype="link"/>
+		</fields>
+		<links>
+			<link field="target_copy" reltype="has_a" key="id" map="" class="acp"/>
+			<link field="part" reltype="has_a" key="id" map="" class="bmp"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="MAP_MONOGRAPH_PART" global_required="true"/>
+                <retrieve/>
+                <update permission="MAP_MONOGRAPH_PART" global_required="true"/>
+                <delete permission="MAP_MONOGRAPH_PART" global_required="true"/>
+            </actions>
+        </permacrud>
+	</class>
 	<class id="aoucd" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::org_unit::closed_date" oils_persist:tablename="actor.org_unit_closed" reporter:label="Closed Dates">
 		<fields oils_persist:primary="id" oils_persist:sequence="actor.org_unit_closed_id_seq">
 			<field name="close_end" reporter:datatype="timestamp" />
@@ -4684,6 +4769,7 @@
 			<field reporter:label="Total Circulations" name="total_circ_count" oils_persist:virtual="true" reporter:datatype="link"/>
 			<field reporter:label="Holds" name="holds" oils_persist:virtual="true" reporter:datatype="link"/>
 			<field reporter:label="Statistical Category Entries" name="stat_cat_entries" oils_persist:virtual="true" reporter:datatype="link"/>
+			<field reporter:label="Monograph Parts" name="parts" oils_persist:virtual="true" reporter:datatype="link"/>
 		</fields>
 		<links>
 			<link field="age_protect" reltype="has_a" key="id" map="" class="crahp"/>
@@ -4700,6 +4786,7 @@
 			<link field="circulations" reltype="has_many" key="target_copy" map="" class="circ"/>
 			<link field="total_circ_count" reltype="might_have" key="id" map="" class="erfcc"/>
 			<link field="circ_modifier" reltype="has_a" key="code" map="" class="ccm"/>
+			<link field="parts" reltype="has_many" key="target_copy" map="part" class="acpm"/>
 		</links>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
             <actions>

Modified: trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm	2011-03-29 15:24:44 UTC (rev 19883)
@@ -713,7 +713,7 @@
 }
 
 sub fetch_callnumber {
-	my( $self, $id ) = @_;
+	my( $self, $id, $flesh ) = @_;
 	my $evt = undef;
 
 	my $e = OpenILS::Event->new( 'ASSET_CALL_NUMBER_NOT_FOUND', id => $id );
@@ -726,6 +726,27 @@
 		'open-ils.cstore.direct.asset.call_number.retrieve', $id );
 	$evt = $e  unless $cn;
 
+    if ($flesh && $cn) {
+        $cn->prefix(
+            $self->simplereq(
+                'open-ils.cstore',
+                'open-ils.cstore.direct.asset.call_number_prefix.retrieve', $cn->prefix
+            )
+        );
+        $cn->suffix(
+            $self->simplereq(
+                'open-ils.cstore',
+                'open-ils.cstore.direct.asset.call_number_suffix.retrieve', $cn->suffix
+            )
+        );
+        $cn->label_class(
+            $self->simplereq(
+                'open-ils.cstore',
+                'open-ils.cstore.direct.asset.call_number_class.retrieve', $cn->label_class
+            )
+        );
+    }
+
 	return ( $cn, $evt );
 }
 

Modified: trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm	2011-03-29 15:24:44 UTC (rev 19883)
@@ -133,7 +133,64 @@
 	return undef;
 }
 
+# if 'delete_maps' is true, the copy->parts data is  treated as the
+# authoritative list for the copy. existing part maps not targeting
+# these parts will be deleted from the DB
+sub update_copy_parts {
+	my($class, $editor, $copy, $delete_maps) = @_;
 
+	return undef if $copy->isdeleted;
+	return undef unless $copy->ischanged or $copy->isnew;
+
+	my $evt;
+	my $incoming_parts = $copy->parts;
+
+	if( $delete_maps ) {
+		$incoming_parts = ($incoming_parts and @$incoming_parts) ? $incoming_parts : [];
+	} else {
+		return undef unless ($incoming_parts and @$incoming_parts);
+	}
+
+	my $maps = $editor->search_asset_copy_part_map({target_copy=>$copy->id});
+
+	if(!$copy->isnew) {
+		# if there is no part map on the copy who's id matches the
+		# current map's id, remove the map from the database
+		for my $map (@$maps) {
+			if(! grep { $_->id == $map->part } @$incoming_parts ) {
+
+				$logger->info("copy update found stale ".
+					"monographic part map ".$map->id. " on copy ".$copy->id);
+
+				$editor->delete_asset_copy_part_map($map)
+					or return $editor->event;
+			}
+		}
+	}
+
+	# go through the part map update/create process
+	for my $incoming_part (@$incoming_parts) { 
+		next unless $incoming_part;
+
+		# if this link already exists in the DB, don't attempt to re-create it
+		next if( grep{$_->part == $incoming_part->id} @$maps );
+	
+		my $new_map = Fieldmapper::asset::copy_part_map->new();
+
+		$new_map->part( $incoming_part->id );
+		$new_map->target_copy( $copy->id );
+
+		$editor->create_asset_copy_part_map($new_map)
+			or return $editor->event;
+
+		$logger->info("copy update created new monographic part copy map ".$editor->data);
+	}
+
+	return undef;
+}
+
+
+
 sub update_copy {
 	my($class, $editor, $override, $vol, $copy, $retarget_holds, $force_delete_empty_bib) = @_;
 
@@ -224,6 +281,9 @@
 		my $sc_entries = $copy->stat_cat_entries;
 		$copy->clear_stat_cat_entries;
 
+        my $parts = $copy->parts;
+		$copy->clear_parts;
+
 		if( $copy->isdeleted ) {
 			$evt = $class->delete_copy($editor, $override, $vol, $copy, $retarget_holds, $force_delete_empty_bib);
 			return $evt if $evt;
@@ -240,6 +300,9 @@
 
 		$copy->stat_cat_entries( $sc_entries );
 		$evt = $class->update_copy_stat_entries($editor, $copy, $delete_stats);
+		$copy->parts( $parts );
+		# probably okay to use $delete_stats here for simplicity
+		$evt = $class->update_copy_parts($editor, $copy, $delete_stats);
 		return $evt if $evt;
 	}
 
@@ -305,6 +368,8 @@
 			owning_lib	=> $vol->owning_lib,
 			record		=> $vol->record,
 			label			=> $vol->label,
+			prefix			=> $vol->prefix,
+			suffix			=> $vol->suffix,
 			deleted		=> 'f'
 		}
 	);
@@ -345,14 +410,17 @@
 
 # returns the volume if it exists
 sub volume_exists {
-    my($class, $e, $rec_id, $label, $owning_lib) = @_;
+    my($class, $e, $rec_id, $label, $owning_lib, $prefix, $suffix) = @_;
     return $e->search_asset_call_number(
-        {label => $label, record => $rec_id, owning_lib => $owning_lib, deleted => 'f'})->[0];
+        {label => $label, record => $rec_id, owning_lib => $owning_lib, deleted => 'f', prefix => $prefix, suffix => $suffix})->[0];
 }
 
 sub find_or_create_volume {
-	my($class, $e, $label, $record_id, $org_id) = @_;
+	my($class, $e, $label, $record_id, $org_id, $prefix, $suffix, $label_class) = @_;
 
+    $prefix ||= '-1';
+    $suffix ||= '-1';
+
     my $vol;
 
     if($record_id == OILS_PRECAT_RECORD) {
@@ -360,7 +428,7 @@
             or return (undef, $e->die_event);
 
     } else {
-        $vol = $class->volume_exists($e, $record_id, $label, $org_id);
+        $vol = $class->volume_exists($e, $record_id, $label, $org_id, $prefix, $suffix);
     }
 
 	# If the volume exists, return the ID
@@ -373,7 +441,10 @@
 
 	$vol = Fieldmapper::asset::call_number->new;
 	$vol->owning_lib($org_id);
+	$vol->label_class($label_class) if ($label_class);
 	$vol->label($label);
+	$vol->prefix($prefix);
+	$vol->suffix($suffix);
 	$vol->record($record_id);
 
     my $evt = OpenILS::Application::Cat::AssetCommon->create_volume(0, $e, $vol);

Modified: trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat.pm	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat.pm	2011-03-29 15:24:44 UTC (rev 19883)
@@ -660,16 +660,23 @@
     $search_hash->{deleted} = 'f';
     my $e = new_editor();
 
-    my $vols = $e->search_asset_call_number([$search_hash, { 'order_by' => {
-        'acn' => 'oils_text_as_bytea(label_sortkey), oils_text_as_bytea(label), id, owning_lib'
-    } } ] );
+    my $vols = $e->search_asset_call_number([
+        $search_hash,
+        {
+            flesh => 1,
+            flesh_fields => { acn => ['prefix','suffix','label_class'] },
+            'order_by' => { 'acn' => 'oils_text_as_bytea(label_sortkey), oils_text_as_bytea(label), id, owning_lib' }
+        }
+    ]);
 
     my @volumes;
 
     for my $volume (@$vols) {
 
-        my $copies = $e->search_asset_copy(
-            { call_number => $volume->id , deleted => 'f' });
+        my $copies = $e->search_asset_copy([
+            { call_number => $volume->id , deleted => 'f' },
+            { flesh => 1, flesh_fields => { acp => ['parts'] } }
+        ]);
 
         $copies = [ sort { $a->barcode cmp $b->barcode } @$copies  ];
 
@@ -912,6 +919,8 @@
         owning_lib => $vol->owning_lib,
         record     => $vol->record,
         label      => $vol->label,
+        prefix     => $vol->prefix,
+        suffix     => $vol->suffix,
         deleted    => 'f',
         id         => {'!=' => $vol->id}
     });
@@ -1034,6 +1043,8 @@
         my $existing_vol = $e->search_asset_call_number(
             {
                 label      => $vol->label, 
+                prefix     => $vol->prefix, 
+                suffix     => $vol->suffix, 
                 record     => $rec, 
                 owning_lib => $o_lib,
                 deleted    => 'f'
@@ -1114,15 +1125,15 @@
 );
 
 sub find_or_create_volume {
-    my( $self, $conn, $auth, $label, $record_id, $org_id ) = @_;
+    my( $self, $conn, $auth, $label, $record_id, $org_id, $prefix, $suffix, $label_class ) = @_;
     my $e = new_editor(authtoken=>$auth, xact=>1);
     return $e->die_event unless $e->checkauth;
     my ($vol, $evt, $exists) = 
-        OpenILS::Application::Cat::AssetCommon->find_or_create_volume($e, $label, $record_id, $org_id);
+        OpenILS::Application::Cat::AssetCommon->find_or_create_volume($e, $label, $record_id, $org_id, $prefix, $suffix, $label_class);
     return $evt if $evt;
     $e->rollback if $exists;
     $e->commit if $vol;
-    return $vol->id;
+    return { 'acn_id' => $vol->id, 'existed' => $exists };
 }
 
 

Modified: trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm	2011-03-29 15:24:44 UTC (rev 19883)
@@ -183,6 +183,8 @@
         return $e->die_event unless $e->allowed('TITLE_HOLDS',  $porg);
     } elsif ( $t eq OILS_HOLD_TYPE_VOLUME ) {
         return $e->die_event unless $e->allowed('VOLUME_HOLDS', $porg);
+    } elsif ( $t eq OILS_HOLD_TYPE_MONOPART ) {
+        return $e->die_event unless $e->allowed('TITLE_HOLDS', $porg);
     } elsif ( $t eq OILS_HOLD_TYPE_ISSUANCE ) {
         return $e->die_event unless $e->allowed('ISSUANCE_HOLDS', $porg);
     } elsif ( $t eq OILS_HOLD_TYPE_COPY ) {
@@ -1947,6 +1949,7 @@
  pickup_lib   - destination for hold, fallback value for selection_ou
  selection_ou - ID of org_unit establishing hard and soft hold boundary settings
  issuanceid   - ID of the issuance to be held, required for Issuance level hold
+ partid       - ID of the monograph part to be held, required for monograph part level hold
  titleid      - ID (BRN) of the title to be held, required for Title level hold
  volume_id    - required for Volume level hold
  copy_id      - required for Copy level hold
@@ -2038,6 +2041,7 @@
     my($e, $patron, $request_lib, $depth, %params) = @_;
 
     my $issuanceid   = $params{issuanceid}      || "";
+    my $partid       = $params{partid}      || "";
     my $titleid      = $params{titleid}      || "";
     my $volid        = $params{volume_id};
     my $copyid       = $params{copy_id};
@@ -2082,6 +2086,12 @@
 			$issuanceid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
         );
 
+	} elsif( $hold_type eq OILS_HOLD_TYPE_MONOPART ) {
+
+		return _check_monopart_hold_is_possible(
+			$partid, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou
+        );
+
 	} elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
 
 		my $maps = $e->search_metabib_metarecord_source_map({metarecord=>$mrid});
@@ -2379,7 +2389,141 @@
     return @status;
 }
 
+sub _check_monopart_hold_is_possible {
+    my( $partid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
+   
+    my $e = new_editor();
+    my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
 
+    # this monster will grab the id and circ_lib of all of the "holdable" copies for the given record
+    my $copies = $e->json_query(
+        { 
+            select => { acp => ['id', 'circ_lib'] },
+              from => {
+                acp => {
+                    acpm => {
+                        field  => 'target_copy',
+                        fkey   => 'id',
+                        filter => { part => $partid }
+                    },
+                    acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
+                    ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   }
+                }
+            }, 
+            where => {
+                '+acp' => { circulate => 't', deleted => 'f', holdable => 't', %org_filter }
+            },
+            distinct => 1
+        }
+    );
+
+    $logger->info("monopart possible found ".scalar(@$copies)." potential copies");
+
+    my $empty_ok;
+    if (!@$copies) {
+        $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
+        $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
+
+        return (
+            0, 0, [
+                new OpenILS::Event(
+                    "HIGH_LEVEL_HOLD_HAS_NO_COPIES",
+                    "payload" => {"fail_part" => "no_ultimate_items"}
+                )
+            ]
+        ) unless $empty_ok;
+
+        return (1, 0);
+    }
+
+    # -----------------------------------------------------------------------
+    # sort the copies into buckets based on their circ_lib proximity to 
+    # the patron's home_ou.  
+    # -----------------------------------------------------------------------
+
+    my $home_org = $patron->home_ou;
+    my $req_org = $request_lib->id;
+
+    $logger->info("prox cache $home_org " . $prox_cache{$home_org});
+
+    $prox_cache{$home_org} = 
+        $e->search_actor_org_unit_proximity({from_org => $home_org})
+        unless $prox_cache{$home_org};
+    my $home_prox = $prox_cache{$home_org};
+
+    my %buckets;
+    my %hash = map { ($_->to_org => $_->prox) } @$home_prox;
+    push( @{$buckets{ $hash{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
+
+    my @keys = sort { $a <=> $b } keys %buckets;
+
+
+    if( $home_org ne $req_org ) {
+      # -----------------------------------------------------------------------
+      # shove the copies close to the request_lib into the primary buckets 
+      # directly before the farthest away copies.  That way, they are not 
+      # given priority, but they are checked before the farthest copies.
+      # -----------------------------------------------------------------------
+        $prox_cache{$req_org} = 
+            $e->search_actor_org_unit_proximity({from_org => $req_org})
+            unless $prox_cache{$req_org};
+        my $req_prox = $prox_cache{$req_org};
+
+        my %buckets2;
+        my %hash2 = map { ($_->to_org => $_->prox) } @$req_prox;
+        push( @{$buckets2{ $hash2{$_->{circ_lib}} } }, $_->{id} ) for @$copies;
+
+        my $highest_key = $keys[@keys - 1];  # the farthest prox in the exising buckets
+        my $new_key = $highest_key - 0.5; # right before the farthest prox
+        my @keys2   = sort { $a <=> $b } keys %buckets2;
+        for my $key (@keys2) {
+            last if $key >= $highest_key;
+            push( @{$buckets{$new_key}}, $_ ) for @{$buckets2{$key}};
+        }
+    }
+
+    @keys = sort { $a <=> $b } keys %buckets;
+
+    my $title;
+    my %seen;
+    my @status;
+    OUTER: for my $key (@keys) {
+      my @cps = @{$buckets{$key}};
+
+      $logger->info("looking at " . scalar(@{$buckets{$key}}). " copies in proximity bucket $key");
+
+      for my $copyid (@cps) {
+
+         next if $seen{$copyid};
+         $seen{$copyid} = 1; # there could be dupes given the merged buckets
+         my $copy = $e->retrieve_asset_copy($copyid);
+         $logger->debug("looking at bucket_key=$key, copy $copyid : circ_lib = " . $copy->circ_lib);
+
+         unless($title) { # grab the title if we don't already have it
+            my $vol = $e->retrieve_asset_call_number(
+               [ $copy->call_number, { flesh => 1, flesh_fields => { bre => ['fixed_fields'], acn => ['record'] } } ] );
+            $title = $vol->record;
+         }
+   
+         @status = verify_copy_for_hold(
+            $patron, $requestor, $title, $copy, $pickup_lib, $request_lib);
+
+         last OUTER if $status[0];
+      }
+    }
+
+    if (!$status[0]) {
+        if (!defined($empty_ok)) {
+            $empty_ok = $e->retrieve_config_global_flag('circ.holds.empty_part_ok');
+            $empty_ok = ($empty_ok and $U->is_true($empty_ok->enabled));
+        }
+
+        return (1,0) if ($empty_ok);
+    }
+    return @status;
+}
+
+
 sub _check_volume_hold_is_possible {
 	my( $vol, $title, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou ) = @_;
     my %org_filter = create_ranged_org_filter(new_editor(), $selection_ou, $depth);
@@ -2688,7 +2832,7 @@
 	$hold->usr($user->id);
 
 
-	my( $mvr, $volume, $copy, $issuance, $bre ) = find_hold_mvr($e, $hold, $args->{suppress_mvr});
+	my( $mvr, $volume, $copy, $issuance, $part, $bre ) = find_hold_mvr($e, $hold, $args->{suppress_mvr});
 
 	flesh_hold_notices([$hold], $e) unless $args->{suppress_notices};
 	flesh_hold_transits([$hold]) unless $args->{suppress_transits};
@@ -2697,12 +2841,15 @@
 
     my $resp = {
         hold           => $hold,
-        copy           => $copy,
-        volume         => $volume,
+        ($copy     ? (copy           => $copy)     : ()),
+        ($volume   ? (volume         => $volume)   : ()),
+        ($issuance ? (issuance       => $issuance) : ()),
+        ($part     ? (part           => $part)     : ()),
+        ($args->{include_bre}  ?  (bre => $bre)    : ()),
+        ($args->{suppress_mvr} ?  () : (mvr => $mvr)),
         %$details
     };
 
-    $resp->{mvr} = $mvr unless $args->{suppress_mvr};
     unless($args->{suppress_patron_details}) {
 	    my $card = $e->retrieve_actor_card($user->card) or return $e->event;
         $resp->{patron_first}   = $user->first_given_name,
@@ -2711,8 +2858,6 @@
         $resp->{patron_alias}   = $user->alias,
     };
 
-    $resp->{bre} = $bre if $args->{include_bre};
-
     return $resp;
 }
 
@@ -2729,6 +2874,7 @@
 	my $copy;
 	my $volume;
     my $issuance;
+    my $part;
 
 	if( $hold->hold_type eq OILS_HOLD_TYPE_METARECORD ) {
 		my $mr = $e->retrieve_metabib_metarecord($hold->target)
@@ -2751,6 +2897,14 @@
 
         $tid = $issuance->subscription->record_entry;
 
+    } elsif( $hold->hold_type eq OILS_HOLD_TYPE_MONOPART ) {
+        $part = $e->retrieve_biblio_monographic_part([
+            $hold->target,
+            {flesh => 1, flesh_fields => {bmp => [ qw/record/ ]}}
+        ]) or return $e->event;
+
+        $tid = $part->record;
+
 	} elsif( $hold->hold_type eq OILS_HOLD_TYPE_COPY ) {
 		$copy = $e->retrieve_asset_copy([
             $hold->target, 
@@ -2772,7 +2926,7 @@
 
     # TODO return metarcord mvr for M holds
 	my $title = $e->retrieve_biblio_record_entry($tid);
-	return ( ($no_mvr) ? undef : $U->record_to_mvr($title), $volume, $copy, $issuance, $title );
+	return ( ($no_mvr) ? undef : $U->record_to_mvr($title), $volume, $copy, $issuance, $part, $title );
 }
 
 __PACKAGE__->register_method(
@@ -3139,6 +3293,14 @@
 
         $query->{where}->{'+acp'}->{call_number} = $hold_target;
 
+     } elsif($hold_type eq 'P') {
+
+        $query->{from}->{acp}->{acpm} = {
+            field  => 'target_copy',
+            fkey   => 'id',
+            filter => {part => $hold_target},
+        };
+
      } elsif($hold_type eq 'I') {
 
         $query->{from}->{acp}->{sitem} = {
@@ -3305,7 +3467,7 @@
                 '-or' => [
                     {
                         '-and' => {
-                            hold_type => 'C',
+                            hold_type => [qw/C F R/],
                             target => {
                                 in => {
                                     select => {acp => ['id']},

Modified: trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm	2011-03-29 15:24:44 UTC (rev 19883)
@@ -973,8 +973,8 @@
 			{
 				flesh => 2,
 				flesh_fields => {
-					acp => ['call_number'],
-					acn => ['record']
+					acp => ['call_number','parts'],
+					acn => ['record','prefix','suffix','label_class']
 				}
 			}
 		]) or return $e->event;

Modified: trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm	2011-03-29 15:24:44 UTC (rev 19883)
@@ -446,7 +446,7 @@
 		"open-ils.cstore.direct.asset.copy.search.atomic",
 		{ id => $ids },
 		{ flesh => 1, 
-		  flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries / ] }
+		  flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries parts / ] }
 		});
 }
 
@@ -494,7 +494,7 @@
                 flesh        => 2,
                 flesh_fields => {
                     acp => [
-                        qw/ location status stat_cat_entry_copy_maps notes age_protect /
+                        qw/ location status stat_cat_entry_copy_maps notes age_protect parts /
                     ],
                     ascecm => [qw/ stat_cat stat_cat_entry /],
                 }
@@ -2227,6 +2227,21 @@
 }
 
 __PACKAGE__->register_method(
+    method        => "fetch_fleshed_cn",
+    api_name      => "open-ils.search.callnumber.fleshed.retrieve",
+    authoritative => 1,
+    notes         => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
+);
+
+sub fetch_fleshed_cn {
+	my( $self, $client, $id ) = @_;
+	my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1 );
+	return $evt if $evt;
+	return $cn;
+}
+
+
+__PACKAGE__->register_method(
     method    => "fetch_copy_by_cn",
     api_name  => 'open-ils.search.copies_by_call_number.retrieve',
     signature => q/
@@ -2342,8 +2357,54 @@
     return \@res;
 }
 
+__PACKAGE__->register_method(
+    method    => 'rec_hold_parts',
+    api_name  => 'open-ils.search.biblio.record_hold_parts',
+    signature => q/
+       Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
+	/
+);
 
+sub rec_hold_parts {
+	my( $self, $conn, $args ) = @_;
 
+    my $rec        = $$args{record};
+    my $mrec       = $$args{metarecord};
+    my $pickup_lib = $$args{pickup_lib};
+    my $e = new_editor();
+
+    my $query = {
+        select => {bmp => ['id', 'label']},
+        from => 'bmp',
+        where => {
+            id => {
+                in => {
+                    select => {'acpm' => ['part']},
+                    from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
+                    where => {
+                        '+acp' => {'deleted' => 'f'},
+                        '+bre' => {id => $rec}
+                    },
+                    distinct => 1,
+                }
+            }
+        }
+    };
+
+    if(defined $pickup_lib) {
+        my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
+        if($hard_boundary) {
+            my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
+            $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
+        }
+    }
+
+    return $e->json_query($query);
+}
+
+
+
+
 __PACKAGE__->register_method(
     method    => 'rec_to_mr_rec_descriptors',
     api_name  => 'open-ils.search.metabib.record_to_descriptors',

Modified: trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Serial.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Serial.pm	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Serial.pm	2011-03-29 15:24:44 UTC (rev 19883)
@@ -1360,6 +1360,7 @@
 sub _find_or_create_call_number {
     my ($e, $lib, $cn_string, $record) = @_;
 
+    # FIXME: should suffix and prefix come into play here?
     my $existing = $e->search_asset_call_number({
         "owning_lib" => $lib,
         "label" => $cn_string,

Modified: trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/asset.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/asset.pm	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/asset.pm	2011-03-29 15:24:44 UTC (rev 19883)
@@ -21,6 +21,22 @@
 __PACKAGE__->columns( Essential => qw/location org position/ );
 
 #-------------------------------------------------------------------------------
+package asset::call_number_suffix;
+use base qw/asset/;
+
+__PACKAGE__->table( 'asset_call_number_suffix' );
+__PACKAGE__->columns( Primary => qw/id/ );
+__PACKAGE__->columns( Essential => qw/owning_lib label label_sortkey/ );
+
+#-------------------------------------------------------------------------------
+package asset::call_number_prefix;
+use base qw/asset/;
+
+__PACKAGE__->table( 'asset_call_number_prefix' );
+__PACKAGE__->columns( Primary => qw/id/ );
+__PACKAGE__->columns( Essential => qw/owning_lib label label_sortkey/ );
+
+#-------------------------------------------------------------------------------
 package asset::call_number_class;
 use base qw/asset/;
 
@@ -34,7 +50,7 @@
 
 __PACKAGE__->table( 'asset_call_number' );
 __PACKAGE__->columns( Primary => qw/id/ );
-__PACKAGE__->columns( Essential => qw/record label creator create_date editor
+__PACKAGE__->columns( Essential => qw/record label creator create_date editor prefix suffix
 				   edit_date record label owning_lib deleted label_class label_sortkey/ );
 
 #-------------------------------------------------------------------------------
@@ -59,6 +75,14 @@
 				   age_protect floating cost status_changed_time/ );
 
 #-------------------------------------------------------------------------------
+package asset::copy_part_map;
+use base qw/asset/;
+
+__PACKAGE__->table( 'asset_copy_part_map' );
+__PACKAGE__->columns( Primary => qw/id/ );
+__PACKAGE__->columns( Essential => qw/target_copy part/);
+
+#-------------------------------------------------------------------------------
 package asset::stat_cat;
 use base qw/asset/;
 

Modified: trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/biblio.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/biblio.pm	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/biblio.pm	2011-03-29 15:24:44 UTC (rev 19883)
@@ -22,5 +22,13 @@
 					editor create_date edit_date pub/ );
 #-------------------------------------------------------------------------------
 
+#-------------------------------------------------------------------------------
+package biblio::monograph_part;
+use base qw/biblio/;
+
+biblio::monograph_part->table( 'biblio_monograph_part' );
+biblio::monograph_part->columns( Essential => qw/id record label label_sortkey/ );
+#-------------------------------------------------------------------------------
+
 1;
 

Modified: trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI.pm	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI.pm	2011-03-29 15:24:44 UTC (rev 19883)
@@ -640,6 +640,15 @@
 	asset::call_number->has_many( copies => 'asset::copy' );
 	asset::call_number->has_many( notes => 'asset::call_number_note' );
 
+	asset::call_number->has_a( prefix => 'asset::call_number_prefix' );
+	asset::call_number->has_a( suffix => 'asset::call_number_suffix' );
+
+	asset::call_number_prefix->has_a( owning_lib => 'actor::org_unit' );
+	asset::call_number_suffix->has_a( owning_lib => 'actor::org_unit' );
+
+	asset::call_number_prefix->has_many( call_numbers => 'asset::call_number' );
+	asset::call_number_suffix->has_many( call_numbers => 'asset::call_number' );
+
 	authority::record_entry->has_many( record_descriptor => 'authority::record_descriptor' );
 	authority::record_entry->has_many( notes => 'authority::record_note' );
 

Modified: trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/dbi.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/dbi.pm	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/dbi.pm	2011-03-29 15:24:44 UTC (rev 19883)
@@ -1,5 +1,16 @@
 {
 
+    #-------------------------------------------------------------------------------
+    package asset::copy_part_map;
+
+    asset::copy_part_map->table( 'asset.copy_part_map' );
+
+    #-------------------------------------------------------------------------------
+    package biblio::monograph_part;
+
+    biblio::monograph_part->table( 'biblio.monograph_part' );
+    biblio::monograph_part->sequence( 'biblio.monograph_part_id_seq' );
+
 	#-------------------------------------------------------------------------------
 	package container::user_bucket;
 
@@ -322,6 +333,18 @@
 	asset::call_number->sequence( 'asset.call_number_id_seq' );
 	
 	#---------------------------------------------------------------------
+	package asset::call_number_suffix;
+	
+	asset::call_number_suffix->table( 'asset.call_number_suffix' );
+	asset::call_number_suffix->sequence( 'asset.call_number_suffix_id_seq' );
+
+	#---------------------------------------------------------------------
+	package asset::call_number_prefix;
+	
+	asset::call_number_prefix->table( 'asset.call_number_prefix' );
+	asset::call_number_prefix->sequence( 'asset.call_number_prefix_id_seq' );
+
+	#---------------------------------------------------------------------
 	package asset::call_number_class;
 	
 	asset::call_number_class->table( 'asset.call_number_class' );

Modified: trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm	2011-03-29 15:24:44 UTC (rev 19883)
@@ -1233,6 +1233,15 @@
 						{ id => [map {$_->id} @{ $vtree->copies }],
 						  deleted => 'f' }
 					) if ($vtree && @{ $vtree->copies });
+
+			} elsif ($hold->hold_type eq 'P') {
+				my @part_maps = asset::copy_part_map->search_where( { part => $hold->target } );
+				$all_copies = [
+					asset::copy->search_where(
+						{ id => [map {$_->target_copy} @part_maps],
+						  deleted => 'f' }
+					)
+				] if (@part_maps);
 					
 			} elsif ($hold->hold_type eq 'I') {
 				my ($itree) = $self

Modified: trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/biblio.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/biblio.pm	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/biblio.pm	2011-03-29 15:24:44 UTC (rev 19883)
@@ -380,19 +380,28 @@
 	my $descendants = "actor.org_unit_descendants(?,?)";
 
 	my $cn_table = asset::call_number->table;
+	my $cnp_table = asset::call_number_prefix->table;
+	my $cns_table = asset::call_number_prefix->table;
 	my $cp_table = asset::copy->table;
 	my $cl_table = asset::copy_location->table;
 	my $cs_table = config::copy_status->table;
 
 	my $sql = <<"	SQL";
 
-		SELECT	cp.circ_lib, cn.label, cp.status, count(cp.id)
+		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,
+				cp.status,
+				count(cp.id)
 		  FROM	$cp_table cp,
 		  	$cn_table cn,
+		  	$cns_table cns,
+		  	$cnp_table cnp,
 			$cl_table cl,
 			$cs_table cs,
 			$descendants d
 		  WHERE	cn.record = ?
+		  	AND cnp.id = cn.prefix
+		  	AND cns.id = cn.suffix
 		  	AND cp.call_number = cn.id
 		  	AND cp.location = cl.id
 			AND cp.circ_lib = d.id
@@ -440,6 +449,8 @@
 	my $descendants = "actor.org_unit_descendants(?,?)";
 
 	my $cn_table = asset::call_number->table;
+	my $cnp_table = asset::call_number_prefix->table;
+	my $cns_table = asset::call_number_prefix->table;
 	my $cp_table = asset::copy->table;
 	my $cl_table = asset::copy_location->table;
 	my $cs_table = config::copy_status->table;
@@ -450,16 +461,20 @@
 	my $sql = <<"	SQL";
 
 		SELECT	cp.circ_lib,
-				cn.label, 
+				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)
 		  FROM	$cp_table cp,
 		  	$cn_table cn,
+		  	$cns_table cns,
+		  	$cnp_table cnp,
 			$cl_table cl,
 			$cs_table cs,
 			$descendants d
 		  WHERE	cn.record = ?
+		  	AND cnp.id = cn.prefix
+		  	AND cns.id = cn.suffix
 		  	AND cp.call_number = cn.id
 		  	AND cp.location = cl.id
 			AND cp.circ_lib = d.id

Modified: trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/SuperCat.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/SuperCat.pm	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/perlmods/lib/OpenILS/Application/SuperCat.pm	2011-03-29 15:24:44 UTC (rev 19883)
@@ -330,7 +330,7 @@
               @cp_filter
 			},
 			{ flesh		=> 1,
-			  flesh_fields	=> { acn => [qw/record owning_lib/] },
+			  flesh_fields	=> { acn => [qw/record owning_lib prefix suffix/] },
 			  order_by	=> { acn => "oils_text_as_bytea(label_sortkey) desc, oils_text_as_bytea(label) desc, id desc, owning_lib desc" },
 			  limit		=> $before_limit,
 			  offset	=> abs($page) * $page_size - $before_offset,
@@ -348,7 +348,7 @@
               @cp_filter
 			},
 			{ flesh		=> 1,
-			  flesh_fields	=> { acn => [qw/record owning_lib/] },
+			  flesh_fields	=> { acn => [qw/record owning_lib prefix suffix/] },
 			  order_by	=> { acn => "oils_text_as_bytea(label_sortkey), oils_text_as_bytea(label), id, owning_lib" },
 			  limit		=> $after_limit,
 			  offset	=> abs($page) * $page_size - $after_offset,
@@ -455,7 +455,7 @@
               @cp_filter
 			},
 			{ flesh		=> 1,
-			  flesh_fields	=> { acn => [qw/record owning_lib/] },
+			  flesh_fields	=> { acn => [qw/record owning_lib prefix suffix/] },
 			  order_by	=> { acn => "oils_text_as_bytea(label_sortkey) desc, oils_text_as_bytea(label) desc, id desc, owning_lib desc" },
 			  limit		=> $limit,
 			  offset	=> $offset,
@@ -473,7 +473,7 @@
               @cp_filter
 			},
 			{ flesh		=> 1,
-			  flesh_fields	=> { acn => [qw/record owning_lib/] },
+			  flesh_fields	=> { acn => [qw/record owning_lib prefix suffix/] },
 			  order_by	=> { acn => "oils_text_as_bytea(label_sortkey), oils_text_as_bytea(label), id, owning_lib" },
 			  limit		=> $limit,
 			  offset	=> $offset,
@@ -1690,7 +1690,7 @@
         		  flesh_fields	=> {
 	        	  			auri    => [qw/call_number_maps/],
 	        	  			auricnm	=> [qw/call_number/],
-	        	  			acn	    => [qw/owning_lib record/],
+	        	  			acn	    => [qw/owning_lib record prefix suffix/],
     				}
 	    	    })
             ->gather(1))
@@ -1731,8 +1731,8 @@
 	    	    $cpid,
     		    { flesh		=> 2,
         		  flesh_fields	=> {
-	        	  			acn	=> [qw/owning_lib record/],
-		        			acp	=> [qw/call_number location status circ_lib stat_cat_entries notes/],
+	        	  			acn	=> [qw/owning_lib record prefix suffix/],
+		        			acp	=> [qw/call_number location status circ_lib stat_cat_entries notes parts/],
     				}
 	    	    })
             ->gather(1))
@@ -1774,9 +1774,9 @@
 	    	    $cnid,
     		    { flesh		=> 5,
         		  flesh_fields	=> {
-	        	  			acn	=> [qw/owning_lib record copies uri_maps/],
+	        	  			acn	=> [qw/owning_lib record copies uri_maps prefix suffix/],
 	        	  			auricnm	=> [qw/uri/],
-		        			acp	=> [qw/location status circ_lib stat_cat_entries notes/],
+		        			acp	=> [qw/location status circ_lib stat_cat_entries notes parts/],
     				}
 	    	    })
             ->gather(1))
@@ -1823,8 +1823,8 @@
 		{ flesh		=> 5,
 		  flesh_fields	=> {
 					bre	=> [qw/call_numbers/],
-		  			acn	=> [qw/copies owning_lib/],
-					acp	=> [qw/location status circ_lib/],
+		  			acn	=> [qw/copies owning_lib prefix suffix/],
+					acp	=> [qw/location status circ_lib parts/],
 				}
 		}
 	)->gather(1);
@@ -1975,9 +1975,9 @@
         },
 		{ flesh		=> 5,
 		  flesh_fields	=> {
-		  			acn	=> [qw/copies owning_lib uri_maps/],
+		  			acn	=> [qw/copies owning_lib uri_maps prefix suffix/],
 		  			auricnm	=> [qw/uri/],
-					acp	=> [qw/circ_lib location status stat_cat_entries notes/],
+					acp	=> [qw/circ_lib location status stat_cat_entries notes parts/],
 					asce	=> [qw/stat_cat/],
 				},
           ( $limit > -1 ? ( limit  => $limit  ) : () ),
@@ -2049,7 +2049,7 @@
 					sstr	=> [qw/items/],
 					sitem	=> [qw/notes unit/],
 					sunit	=> [qw/notes location status circ_lib stat_cat_entries call_number/],
-					acn	=> [qw/owning_lib/],
+					acn	=> [qw/owning_lib prefix suffix/],
 				},
           ( $limit > -1 ? ( limit  => $limit  ) : () ),
           ( $offset     ? ( offset => $offset ) : () ),
@@ -3021,6 +3021,20 @@
     }
 
 
+    $xml .= '      <prefix ';
+    $xml .= 'ident="' . $self->obj->prefix->id . '" ';
+    $xml .= 'id="tag:open-ils.org:asset-call_number_prefix/' . $self->obj->prefix->id . '" ';
+    $xml .= 'label_sortkey="'.$self->escape( $self->obj->prefix->label_sortkey ) .'">';
+    $xml .= $self->escape( $self->obj->prefix->label ) .'</prefix>';
+    $xml .= "\n";
+
+    $xml .= '      <suffix ';
+    $xml .= 'ident="' . $self->obj->suffix->id . '" ';
+    $xml .= 'id="tag:open-ils.org:asset-call_number_suffix/' . $self->obj->suffix->id . '" ';
+    $xml .= 'label_sortkey="'.$self->escape( $self->obj->suffix->label_sortkey ) .'">';
+    $xml .= $self->escape( $self->obj->suffix->label ) .'</suffix>';
+    $xml .= "\n";
+
     $xml .= '      <owning_lib xmlns="http://open-ils.org/spec/actors/v1" ';
     $xml .= 'id="tag:open-ils.org:actor-org_unit/' . $self->obj->owning_lib->id . '" ';
     $xml .= 'shortname="'.$self->escape( $self->obj->owning_lib->shortname ) .'" ';
@@ -3453,6 +3467,15 @@
     $xml .= 'name="'.$self->escape( $self->obj->circ_lib->name ) .'" opac_visible="'.$self->obj->circ_lib->opac_visible.'"/>';
     $xml .= "\n";
 
+	$xml .= "        <monograph_parts>\n";
+	if (ref($self->obj->parts) && $self->obj->parts) {
+		for my $part ( @{$self->obj->parts} ) {
+			$xml .= sprintf('        <monograph_part record="%s" sortkey="%s">%s</monograph_part>',$part->record, $self->escape($part->label_sortkey), $self->escape($part->label));
+			$xml .= "\n";
+		}
+	}
+
+	$xml .= "        </monograph_parts>\n";
 	$xml .= "        <copy_notes>\n";
 	if (ref($self->obj->notes) && $self->obj->notes) {
 		for my $note ( @{$self->obj->notes} ) {

Modified: trunk/Open-ILS/src/perlmods/lib/OpenILS/Const.pm
===================================================================
--- trunk/Open-ILS/src/perlmods/lib/OpenILS/Const.pm	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/perlmods/lib/OpenILS/Const.pm	2011-03-29 15:24:44 UTC (rev 19883)
@@ -102,6 +102,7 @@
 econst OILS_HOLD_TYPE_VOLUME      => 'V';
 econst OILS_HOLD_TYPE_TITLE       => 'T';
 econst OILS_HOLD_TYPE_METARECORD  => 'M';
+econst OILS_HOLD_TYPE_MONOPART    => 'P';
 
 
 econst OILS_BILLING_TYPE_OVERDUE_MATERIALS => 'Overdue materials';

Modified: trunk/Open-ILS/src/sql/Pg/002.schema.config.sql
===================================================================
--- trunk/Open-ILS/src/sql/Pg/002.schema.config.sql	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/sql/Pg/002.schema.config.sql	2011-03-29 15:24:44 UTC (rev 19883)
@@ -70,7 +70,7 @@
     install_date    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
 );
 
-INSERT INTO config.upgrade_log (version) VALUES ('0503'); -- miker for tsbere
+INSERT INTO config.upgrade_log (version) VALUES ('0504'); -- miker for tsbere
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,

Modified: trunk/Open-ILS/src/sql/Pg/010.schema.biblio.sql
===================================================================
--- trunk/Open-ILS/src/sql/Pg/010.schema.biblio.sql	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/sql/Pg/010.schema.biblio.sql	2011-03-29 15:24:44 UTC (rev 19883)
@@ -79,4 +79,30 @@
 CREATE INDEX biblio_record_note_creator_idx ON biblio.record_note ( creator );
 CREATE INDEX biblio_record_note_editor_idx ON biblio.record_note ( editor );
 
+CREATE TABLE biblio.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(
+        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();
+
 COMMIT;

Modified: trunk/Open-ILS/src/sql/Pg/040.schema.asset.sql
===================================================================
--- trunk/Open-ILS/src/sql/Pg/040.schema.asset.sql	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/sql/Pg/040.schema.asset.sql	2011-03-29 15:24:44 UTC (rev 19883)
@@ -91,6 +91,13 @@
 CREATE INDEX cp_create_date  ON asset.copy (create_date);
 CREATE RULE protect_copy_delete AS ON DELETE TO asset.copy DO INSTEAD UPDATE asset.copy SET deleted = TRUE WHERE OLD.id = asset.copy.id;
 
+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.opac_visible_copies (
   id        BIGINT primary key, -- copy id
   record    BIGINT,
@@ -279,6 +286,42 @@
     ('Library of Congress (LC)', 'asset.label_normalizer_lc', '050ab,055ab,090abef')
 ;
 
+CREATE OR REPLACE FUNCTION asset.normalize_affix_sortkey () RETURNS TRIGGER AS $$
+BEGIN
+    NEW.label_sortkey := REGEXP_REPLACE(
+        lpad_number_substrings(
+            naco_normalize(NEW.label),
+            '0',
+            10
+        ),
+        E'\\s+',
+        '',
+        'g'
+    );
+    RETURN NEW;
+END;
+$$ LANGUAGE PLPGSQL;
+
+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 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);
+
 CREATE TABLE asset.call_number (
 	id		bigserial PRIMARY KEY,
 	creator		BIGINT				NOT NULL,
@@ -286,9 +329,11 @@
 	editor		BIGINT				NOT NULL,
 	edit_date	TIMESTAMP WITH TIME ZONE	DEFAULT NOW(),
 	record		bigint				NOT NULL,
-	owning_lib	INT				NOT NULL,
+	owning_lib	INT				    NOT NULL,
 	label		TEXT				NOT NULL,
 	deleted		BOOL				NOT NULL DEFAULT FALSE,
+	prefix  	INT				    NOT NULL DEFAULT -1 REFERENCES asset.call_number_prefix(id) DEFERRABLE INITIALLY DEFERRED,
+	suffix  	INT				    NOT NULL DEFAULT -1 REFERENCES asset.call_number_suffix(id) DEFERRABLE INITIALLY DEFERRED,
 	label_class	BIGINT				DEFAULT 1 NOT NULL
 							REFERENCES asset.call_number_class(id)
 							DEFERRABLE INITIALLY DEFERRED,
@@ -300,7 +345,7 @@
 CREATE INDEX asset_call_number_dewey_idx ON asset.call_number (public.call_number_dewey(label));
 CREATE INDEX asset_call_number_upper_label_id_owning_lib_idx ON asset.call_number (oils_text_as_bytea(label),id,owning_lib);
 CREATE INDEX asset_call_number_label_sortkey ON asset.call_number(oils_text_as_bytea(label_sortkey));
-CREATE UNIQUE INDEX asset_call_number_label_once_per_lib ON asset.call_number (record, owning_lib, label) WHERE deleted = FALSE OR deleted IS FALSE;
+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;
 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;
 CREATE RULE protect_cn_delete AS ON DELETE TO asset.call_number DO INSTEAD UPDATE asset.call_number SET deleted = TRUE WHERE OLD.id = asset.call_number.id;
 CREATE TRIGGER asset_label_sortkey_trigger

Modified: trunk/Open-ILS/src/sql/Pg/950.data.seed-values.sql
===================================================================
--- trunk/Open-ILS/src/sql/Pg/950.data.seed-values.sql	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/sql/Pg/950.data.seed-values.sql	2011-03-29 15:24:44 UTC (rev 19883)
@@ -1588,6 +1588,8 @@
 INSERT INTO asset.copy_location (id, name,owning_lib) VALUES (1, oils_i18n_gettext(1, 'Stacks', 'acpl', 'name'),1);
 SELECT SETVAL('asset.copy_location_id_seq'::TEXT, 100);
 
+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, '');
 INSERT INTO asset.call_number VALUES (-1,1,NOW(),1,NOW(),-1,1,'UNCATALOGED');
 
 -- circ matrix
@@ -7941,3 +7943,15 @@
     (37, 'circ_lib.billing_address')
 ;
 
+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'
+);

Modified: trunk/Open-ILS/src/sql/Pg/990.schema.unapi.sql
===================================================================
--- trunk/Open-ILS/src/sql/Pg/990.schema.unapi.sql	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/src/sql/Pg/990.schema.unapi.sql	2011-03-29 15:24:44 UTC (rev 19883)
@@ -27,6 +27,8 @@
 
 -- 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 ) 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 ) 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 ) 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 ) 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 ) 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 ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
@@ -44,6 +46,7 @@
 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 ) 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 ) 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 ) 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 ) 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) 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, 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;
@@ -230,6 +233,13 @@
                                      ORDER BY 1
                      )x)
                  ),
+                 CASE 
+                     WHEN ('bmp' = ANY ($5)) THEN
+                        XMLELEMENT( name monograph_parts,
+                            XMLAGG((SELECT unapi.bmp( id, 'xml', 'monograph_part', array_remove_item_by_value( array_remove_item_by_value($5,'bre'), 'holdings_xml'), $3, $4, $6, $7) FROM biblio.monograph_part WHERE record = $1))
+                        )
+                     ELSE NULL
+                 END,
                  CASE WHEN ('acn' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 
                      XMLELEMENT(
                          name volumes,
@@ -540,6 +550,39 @@
           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 ) RETURNS XML AS $F$
+        SELECT  XMLELEMENT(
+                    name monograph_part,
+                    XMLATTRIBUTES(
+                        'http://open-ils.org/spec/holdings/v1' AS xmlns,
+                        'tag:open-ils.org:U2 at bmp/' || id AS id,
+                        id AS ident,
+                        label,
+                        label_sortkey,
+                        'tag:open-ils.org:U2 at bre/' || record AS record
+                    ),
+                    CASE 
+                        WHEN ('acp' = ANY ($4)) THEN
+                            XMLELEMENT( name copies,
+                                (SELECT XMLAGG(acp) FROM (
+                                    SELECT  unapi.acp( cp.id, 'xml', 'copy', array_remove_item_by_value($4,'bmp'), $5, $6, $7, $8)
+                                      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', array_remove_item_by_value($4,'bmp'), $5, $6, $7, $8) 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 ) RETURNS XML AS $F$
         SELECT  XMLELEMENT(
                     name copy,
@@ -565,10 +608,17 @@
                     XMLELEMENT( name statcats,
                         CASE 
                             WHEN ('ascecm' = ANY ($4)) THEN
-                                XMLAGG((SELECT unapi.acpn( stat_cat_entry, 'xml', 'statcat', array_remove_item_by_value($4,'acp'), $5, $6, $7, $8) FROM asset.stat_cat_entry_copy_map WHERE owning_copy = cp.id))
+                                XMLAGG((SELECT unapi.ascecm( stat_cat_entry, 'xml', 'statcat', array_remove_item_by_value($4,'acp'), $5, $6, $7, $8) 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', array_remove_item_by_value($4,'acp'), $5, $6, $7, $8) FROM asset.copy_part_map WHERE target_copy = cp.id))
+                            )
+                        ELSE NULL
+                    END
                 )
           FROM  asset.copy cp
           WHERE id = $1
@@ -645,14 +695,46 @@
                         name uris,
                         (SELECT XMLAGG(auri) FROM (SELECT unapi.auri(uri,'xml','uri', array_remove_item_by_value($4,'acn'), $5, $6, $7, $8) FROM asset.uri_call_number_map WHERE call_number = acn.id)x)
                     ),
+                    CASE WHEN ('acnp' = ANY ($4)) THEN unapi.acnp( acn.prefix, 'marcxml', 'prefix', array_remove_item_by_value($4,'acn'), $5, $6, $7, $8) ELSE NULL END,
+                    CASE WHEN ('acns' = ANY ($4)) THEN unapi.acns( acn.suffix, 'marcxml', 'suffix', array_remove_item_by_value($4,'acn'), $5, $6, $7, $8) ELSE NULL END,
                     CASE WHEN ('bre' = ANY ($4)) THEN unapi.bre( acn.record, 'marcxml', 'record', array_remove_item_by_value($4,'acn'), $5, $6, $7, $8) 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;
+          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 ) RETURNS XML AS $F$
+        SELECT  XMLELEMENT(
+                    name call_number_prefix,
+                    XMLATTRIBUTES(
+                        'http://open-ils.org/spec/holdings/v1' AS xmlns,
+                        id AS ident,
+                        label,
+                        label_sortkey
+                    ),
+                    unapi.aou( owning_lib, $2, 'owning_lib', 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 ) RETURNS XML AS $F$
+        SELECT  XMLELEMENT(
+                    name call_number_suffix,
+                    XMLATTRIBUTES(
+                        'http://open-ils.org/spec/holdings/v1' AS xmlns,
+                        id AS ident,
+                        label,
+                        label_sortkey
+                    ),
+                    unapi.aou( owning_lib, $2, 'owning_lib', 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 ) RETURNS XML AS $F$
         SELECT  XMLELEMENT(
                     name volume,

Added: trunk/Open-ILS/src/sql/Pg/upgrade/0504.schema.parts_and_cnaffix.sql
===================================================================
--- trunk/Open-ILS/src/sql/Pg/upgrade/0504.schema.parts_and_cnaffix.sql	                        (rev 0)
+++ trunk/Open-ILS/src/sql/Pg/upgrade/0504.schema.parts_and_cnaffix.sql	2011-03-29 15:24:44 UTC (rev 19883)
@@ -0,0 +1,321 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0504'); -- miker
+
+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(
+        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 OR REPLACE FUNCTION asset.normalize_affix_sortkey () RETURNS TRIGGER AS $$
+BEGIN
+    NEW.label_sortkey := REGEXP_REPLACE(
+        lpad_number_substrings(
+            naco_normalize(NEW.label),
+            '0',
+            10
+        ),
+        E'\\s+',
+        '',
+        'g'
+    );
+    RETURN NEW;
+END;
+$$ LANGUAGE PLPGSQL;
+
+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 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;
+
+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'
+);
+
+
+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 ) RETURNS XML AS $F$
+        SELECT  XMLELEMENT(
+                    name monograph_part,
+                    XMLATTRIBUTES(
+                        'http://open-ils.org/spec/holdings/v1' AS xmlns,
+                        'tag:open-ils.org:U2 at bmp/' || id AS id,
+                        id AS ident,
+                        label,
+                        label_sortkey,
+                        'tag:open-ils.org:U2 at bre/' || record AS record
+                    ),
+                    CASE 
+                        WHEN ('acp' = ANY ($4)) THEN
+                            XMLELEMENT( name copies,
+                                (SELECT XMLAGG(acp) FROM (
+                                    SELECT  unapi.acp( cp.id, 'xml', 'copy', array_remove_item_by_value($4,'bmp'), $5, $6, $7, $8)
+                                      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', array_remove_item_by_value($4,'bmp'), $5, $6, $7, $8) 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.acnp ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL ) RETURNS XML AS $F$
+        SELECT  XMLELEMENT(
+                    name call_number_prefix,
+                    XMLATTRIBUTES(
+                        'http://open-ils.org/spec/holdings/v1' AS xmlns,
+                        id AS ident,
+                        label,
+                        label_sortkey
+                    ),
+                    unapi.aou( owning_lib, $2, 'owning_lib', 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 ) RETURNS XML AS $F$
+        SELECT  XMLELEMENT(
+                    name call_number_suffix,
+                    XMLATTRIBUTES(
+                        'http://open-ils.org/spec/holdings/v1' AS xmlns,
+                        id AS ident,
+                        label,
+                        label_sortkey
+                    ),
+                    unapi.aou( owning_lib, $2, 'owning_lib', 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.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) RETURNS XML AS $F$
+     SELECT  XMLELEMENT(
+                 name holdings,
+                 XMLATTRIBUTES(
+                    'http://open-ils.org/spec/holdings/v1' AS xmlns,
+                    CASE WHEN ('bre' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 'tag:open-ils.org:U2 at 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', array_remove_item_by_value( array_remove_item_by_value($5,'bre'), 'holdings_xml'), $3, $4, $6, $7) 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',array_remove_item_by_value(array_remove_item_by_value('{acn,auri}'::TEXT[] || $5,'holdings_xml'),'bre'), $3, $4, $6, $7)
+                              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)
+                              FROM  serial.subscription
+                              WHERE record_entry = $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 ) RETURNS XML AS $F$
+        SELECT  XMLELEMENT(
+                    name copy,
+                    XMLATTRIBUTES(
+                        'http://open-ils.org/spec/holdings/v1' AS xmlns,
+                        'tag:open-ils.org:U2 at 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', array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
+                    unapi.acl( location, $2, 'location', array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
+                    unapi.aou( circ_lib, $2, 'circ_lib', array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
+                    unapi.aou( circ_lib, $2, 'circlib', array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
+                    CASE WHEN ('acn' = ANY ($4)) THEN unapi.acn( call_number, $2, 'call_number', array_remove_item_by_value($4,'acp'), $5, $6, $7, $8) ELSE NULL END,
+                    XMLELEMENT( name copy_notes,
+                        CASE 
+                            WHEN ('acpn' = ANY ($4)) THEN
+                                XMLAGG((SELECT unapi.acpn( id, 'xml', 'copy_note', 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
+                    ),
+                    XMLELEMENT( name statcats,
+                        CASE 
+                            WHEN ('ascecm' = ANY ($4)) THEN
+                                XMLAGG((SELECT unapi.ascecm( stat_cat_entry, 'xml', 'statcat', array_remove_item_by_value($4,'acp'), $5, $6, $7, $8) 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', array_remove_item_by_value($4,'acp'), $5, $6, $7, $8) 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.acn ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL ) RETURNS XML AS $F$
+        SELECT  XMLELEMENT(
+                    name volume,
+                    XMLATTRIBUTES(
+                        'http://open-ils.org/spec/holdings/v1' AS xmlns,
+                        'tag:open-ils.org:U2 at 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', 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', array_remove_item_by_value($4,'acn'), $5, $6, $7, $8)
+                                      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', array_remove_item_by_value($4,'acn'), $5, $6, $7, $8) FROM asset.uri_call_number_map WHERE call_number = acn.id)x)
+                    ),
+                    CASE WHEN ('acnp' = ANY ($4)) THEN unapi.acnp( acn.prefix, 'marcxml', 'prefix', array_remove_item_by_value($4,'acn'), $5, $6, $7, $8) ELSE NULL END,
+                    CASE WHEN ('acns' = ANY ($4)) THEN unapi.acns( acn.suffix, 'marcxml', 'suffix', array_remove_item_by_value($4,'acn'), $5, $6, $7, $8) ELSE NULL END,
+                    CASE WHEN ('bre' = ANY ($4)) THEN unapi.bre( acn.record, 'marcxml', 'record', array_remove_item_by_value($4,'acn'), $5, $6, $7, $8) 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;
+
+
+COMMIT;
+

Modified: trunk/Open-ILS/web/js/ui/default/acq/common/li_table.js
===================================================================
--- trunk/Open-ILS/web/js/ui/default/acq/common/li_table.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/web/js/ui/default/acq/common/li_table.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -2467,7 +2467,6 @@
                                     copyList.push(copy);
                                 }
                                 if (xulG) {
-                                    // If we need to, we can pass in an update_copy function to handle the update instead of volume_item_creator
                                     xulG.volume_item_creator( { 'existing_copies' : copyList } );
                                 }
                             } catch(E) {

Added: trunk/Open-ILS/web/js/ui/default/conify/global/config/acn_prefix.js
===================================================================
--- trunk/Open-ILS/web/js/ui/default/conify/global/config/acn_prefix.js	                        (rev 0)
+++ trunk/Open-ILS/web/js/ui/default/conify/global/config/acn_prefix.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -0,0 +1,72 @@
+dojo.require('dojox.grid.DataGrid');
+dojo.require('openils.widget.AutoGrid');
+dojo.require('dojox.grid.cells.dijit');
+dojo.require('dojo.data.ItemFileWriteStore');
+dojo.require('dijit.form.CurrencyTextBox');
+dojo.require('dijit.Dialog');
+dojo.require('dojox.widget.PlaceholderMenuItem');
+dojo.require('fieldmapper.OrgUtils');
+dojo.require('dijit.form.FilteringSelect');
+dojo.require('openils.PermaCrud');
+dojo.require('openils.widget.OrgUnitFilteringSelect');
+
+var thingContextOrg;
+var thingList;
+
+/** really need to put this in a shared location... */
+function getOrgInfo(rowIndex, item) {
+    if(!item) return '';
+    var orgId = this.grid.store.getValue(item, this.field);
+    return fieldmapper.aou.findOrgUnit(orgId).shortname();
+}
+
+function thingInit() {
+
+    thingGrid.disableSelectorForRow = function(rowIdx) {
+        var item = thingGrid.getItem(rowIdx);
+        return (thingGrid.store.getValue(item, 'id') < 0);
+    }
+
+    buildGrid();
+    var connect = function() {
+        dojo.connect(thingContextOrgSelect, 'onChange',
+                     function() {
+                         thingContextOrg = this.getValue();
+                         thingGrid.resetStore();
+                         buildGrid();
+                     }
+                    );
+    };
+    // go ahead and let staff see everything
+    new openils.User().buildPermOrgSelector('STAFF_LOGIN', thingContextOrgSelect, null, connect);
+}
+
+function buildGrid() {
+    if(thingContextOrg == null)
+        thingContextOrg = openils.User.user.ws_ou();
+
+    fieldmapper.standardRequest(
+        ['open-ils.pcrud', 'open-ils.pcrud.search.acnp.atomic'],
+        {   async: true,
+            params: [
+                openils.User.authtoken,
+                {"owning_lib":fieldmapper.aou.descendantNodeList(thingContextOrg,true)},
+                {"order_by":{"acnp":"label_sortkey"}}
+            ],
+            oncomplete: function(r) {
+                if(thingList = openils.Util.readResponse(r)) {
+                    thingList = openils.Util.objectSort(thingList);
+                    dojo.forEach(thingList,
+                                 function(e) {
+                                     thingGrid.store.newItem(acnp.toStoreItem(e));
+                                 }
+                                );
+                }
+            }
+        }
+    );
+}
+
+openils.Util.addOnLoad(thingInit);
+
+

Added: trunk/Open-ILS/web/js/ui/default/conify/global/config/acn_suffix.js
===================================================================
--- trunk/Open-ILS/web/js/ui/default/conify/global/config/acn_suffix.js	                        (rev 0)
+++ trunk/Open-ILS/web/js/ui/default/conify/global/config/acn_suffix.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -0,0 +1,72 @@
+dojo.require('dojox.grid.DataGrid');
+dojo.require('openils.widget.AutoGrid');
+dojo.require('dojox.grid.cells.dijit');
+dojo.require('dojo.data.ItemFileWriteStore');
+dojo.require('dijit.form.CurrencyTextBox');
+dojo.require('dijit.Dialog');
+dojo.require('dojox.widget.PlaceholderMenuItem');
+dojo.require('fieldmapper.OrgUtils');
+dojo.require('dijit.form.FilteringSelect');
+dojo.require('openils.PermaCrud');
+dojo.require('openils.widget.OrgUnitFilteringSelect');
+
+var thingContextOrg;
+var thingList;
+
+/** really need to put this in a shared location... */
+function getOrgInfo(rowIndex, item) {
+    if(!item) return '';
+    var orgId = this.grid.store.getValue(item, this.field);
+    return fieldmapper.aou.findOrgUnit(orgId).shortname();
+}
+
+function thingInit() {
+
+    thingGrid.disableSelectorForRow = function(rowIdx) {
+        var item = thingGrid.getItem(rowIdx);
+        return (thingGrid.store.getValue(item, 'id') < 0);
+    }
+
+    buildGrid();
+    var connect = function() {
+        dojo.connect(thingContextOrgSelect, 'onChange',
+                     function() {
+                         thingContextOrg = this.getValue();
+                         thingGrid.resetStore();
+                         buildGrid();
+                     }
+                    );
+    };
+    // go ahead and let staff see everything
+    new openils.User().buildPermOrgSelector('STAFF_LOGIN', thingContextOrgSelect, null, connect);
+}
+
+function buildGrid() {
+    if(thingContextOrg == null)
+        thingContextOrg = openils.User.user.ws_ou();
+
+    fieldmapper.standardRequest(
+        ['open-ils.pcrud', 'open-ils.pcrud.search.acns.atomic'],
+        {   async: true,
+            params: [
+                openils.User.authtoken,
+                {"owning_lib":fieldmapper.aou.descendantNodeList(thingContextOrg,true)},
+                {"order_by":{"acns":"label_sortkey"}}
+            ],
+            oncomplete: function(r) {
+                if(thingList = openils.Util.readResponse(r)) {
+                    thingList = openils.Util.objectSort(thingList);
+                    dojo.forEach(thingList,
+                                 function(e) {
+                                     thingGrid.store.newItem(acns.toStoreItem(e));
+                                 }
+                                );
+                }
+            }
+        }
+    );
+}
+
+openils.Util.addOnLoad(thingInit);
+
+

Modified: trunk/Open-ILS/web/opac/locale/en-US/lang.dtd
===================================================================
--- trunk/Open-ILS/web/opac/locale/en-US/lang.dtd	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/web/opac/locale/en-US/lang.dtd	2011-03-29 15:24:44 UTC (rev 19883)
@@ -284,6 +284,8 @@
 <!ENTITY staff.cat.opac.copy_browse.accesskey "H">
 <!ENTITY staff.cat.opac.copy_browse.label "Holdings Maintenance">
 <!ENTITY staff.cat.opac.default.label "Set bottom interface as Default">
+<!ENTITY staff.cat.opac.manage_parts.accesskey "P">
+<!ENTITY staff.cat.opac.manage_parts.label "Manage Parts">
 <!ENTITY staff.cat.opac.marc_edit.accesskey "E">
 <!ENTITY staff.cat.opac.marc_edit.label "MARC Edit">
 <!ENTITY staff.cat.opac.marc_view.accesskey "V">
@@ -708,6 +710,8 @@
 <!ENTITY staff.main.menu.admin.server_admin.conify.copy_status.label "Copy Statuses">
 <!ENTITY staff.main.menu.admin.server_admin.conify.marc_record_attrs.label "MARC Record Attributes">
 <!ENTITY staff.main.menu.admin.server_admin.conify.coded_value_maps.label "MARC Coded Value Maps">
+<!ENTITY staff.main.menu.admin.server_admin.conify.acn_prefix.label "Call Number Prefixes">
+<!ENTITY staff.main.menu.admin.server_admin.conify.acn_suffix.label "Call Number Suffixes">
 <!ENTITY staff.main.menu.admin.server_admin.conify.billing_type.label "Billing Types">
 <!ENTITY staff.main.menu.admin.server_admin.conify.z3950_source.label "Z39.50 Servers">
 <!ENTITY staff.main.menu.admin.server_admin.conify.circulation_modifier.label "Circulation Modifiers">
@@ -2368,6 +2372,7 @@
 <!ENTITY staff.cat.bib_brief.title.label "Title:">
 <!ENTITY staff.cat.bib_brief.title.accesskey "">
 <!ENTITY staff.cat.bib_brief.view_marc "View MARC">
+<!ENTITY staff.cat.bib_brief.add_volumes "Add Volumes">
 <!ENTITY staff.cat.bib_brief.author.label "Author:">
 <!ENTITY staff.cat.bib_brief.author.accesskey "">
 <!ENTITY staff.cat.bib_brief.edition.label "Edition:">
@@ -2462,7 +2467,7 @@
 <!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">
+<!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.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">
@@ -2486,7 +2491,6 @@
 <!ENTITY staff.cat.copy_browser.actions.sel_mark_items_missing.accesskey "g">
 <!ENTITY staff.cat.copy_browser.actions.cmd_print_spine_labels.label "Print Item Spine Labels">
 <!ENTITY staff.cat.copy_browser.actions.cmd_print_spine_labels.accesskey "P">
-<!ENTITY staff.cat.copy_browser.actions.cmd_replace_barcode.label "Replace Barcode">
 <!ENTITY staff.cat.copy_browser.actions.save_columns.label "Save Columns">
 <!ENTITY staff.cat.copy_browser.actions.cmd_refresh_list.label "Refresh Listing">
 <!ENTITY staff.cat.copy_browser.actions.cmd_refresh_list.accesskey "R">
@@ -2513,7 +2517,7 @@
 
 <!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">
+<!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.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">
@@ -2766,6 +2770,12 @@
 <!ENTITY staff.cat.volume_copy_creator.print_labels.accesskey "P">
 <!ENTITY staff.cat.volume_copy_creator.library_label.value "Library">
 <!ENTITY staff.cat.volume_copy_creator.num_of_volumes_label.value "# of volumes">
+<!ENTITY staff.cat.volume_copy_creator.batch_bar "BATCH">
+<!ENTITY staff.cat.volume_copy_creator.batch_bar.call_number.classification "Classification:">
+<!ENTITY staff.cat.volume_copy_creator.batch_bar.call_number.prefix "Prefix:">
+<!ENTITY staff.cat.volume_copy_creator.batch_bar.call_number.label.label "Label:">
+<!ENTITY staff.cat.volume_copy_creator.batch_bar.call_number.label.accesskey "L">
+<!ENTITY staff.cat.volume_copy_creator.batch_bar.call_number.suffix "Suffix:">
 <!ENTITY staff.cat.volume_editor.title "Volumes">
 <!ENTITY staff.cat.volume_editor.caption.label "Volume Editor">
 <!ENTITY staff.cat.volume_editor.modify.label "Modify">
@@ -2774,6 +2784,11 @@
 <!ENTITY staff.cat.volume_editor.cancel.accesskey "C">
 <!ENTITY staff.cat.volume_editor.automerge.label "Auto-Merge on Volume Collision">
 <!ENTITY staff.cat.volume_editor.automerge.accesskey "A">
+<!ENTITY staff.cat.volume_editor.owning_lib "Owning lib">
+<!ENTITY staff.cat.volume_editor.classification "Classification">
+<!ENTITY staff.cat.volume_editor.prefix "Prefix">
+<!ENTITY staff.cat.volume_editor.label "Label">
+<!ENTITY staff.cat.volume_editor.suffix "Suffix">
 <!ENTITY staff.cat.z3950.marc_import.label "MARC Import via Z39.50">
 <!ENTITY staff.cat.z3950.marc_import.accesskey "I">
 <!ENTITY staff.cat.z3950.service_credentials.label "Service and Credentials">

Modified: trunk/Open-ILS/web/opac/locale/en-US/opac.dtd
===================================================================
--- trunk/Open-ILS/web/opac/locale/en-US/opac.dtd	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/web/opac/locale/en-US/opac.dtd	2011-03-29 15:24:44 UTC (rev 19883)
@@ -202,6 +202,7 @@
 <!--	================================================================= 
 	MyOPAC Holds Page 
 	================================================================= -->
+
 <!ENTITY myopac.holds.formats "Formats">
 <!ENTITY myopac.holds.location "Pickup Location">
 <!ENTITY myopac.holds.edit "Edit">
@@ -488,6 +489,7 @@
 	Rdetail
 	================================================================= -->
 <!ENTITY rdetail.print "print these details">
+<!ENTITY rdetail.cn.part "Part">
 <!ENTITY rdetail.cn.barcode "Barcode">
 <!ENTITY rdetail.cn.location "Location">
 <!ENTITY rdetail.cn.hold.age "Age Hold Protection">
@@ -592,6 +594,7 @@
 <!ENTITY common.call.number.label "Call Number:">
 <!ENTITY common.isbn.label "ISBN:">
 <!ENTITY common.issn.label "ISSN:">
+<!ENTITY common.mono_parts.label "Monograph Parts:">
 <!ENTITY common.copy.barcode.label "Copy Barcode:">
 <!ENTITY common.issuance_label.label "Issuance Label:">
 <!ENTITY common.hold.place "Place hold for my account">

Modified: trunk/Open-ILS/web/opac/skin/default/js/copy_details.js
===================================================================
--- trunk/Open-ILS/web/opac/skin/default/js/copy_details.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/web/opac/skin/default/js/copy_details.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -208,6 +208,7 @@
 function cpdDrawCopy(r) {
 	var copy = r.getResultObject();
 	var row  = r.row;
+    var trow = r.args.templateRow;
 
     if (r.args.copy_location && copy.location().name() != r.args.copy_location) {
         hideMe(row);
@@ -218,6 +219,18 @@
 	$n(row, 'location').appendChild(text(copy.location().name()));
 	$n(row, 'status').appendChild(text(copy.status().name()));
 
+    // append comma-separated list of part this copy is linked to
+    if(copy.parts() && copy.parts().length) {
+        unHideMe($n(trow, 'copy_part_label'));
+        unHideMe($n(row, 'copy_part'));
+        for(var i = 0; i < copy.parts().length; i++) {
+            var part = copy.parts()[i];
+            var node = $n(row, 'copy_part');
+            if(i > 0) node.appendChild(text(','));
+            node.appendChild(text(part.label()));
+        }
+    }
+
 	if(isXUL()) {
 		/* show the hold link */
 		var l = $n(row, 'copy_hold_link');

Modified: trunk/Open-ILS/web/opac/skin/default/js/holds.js
===================================================================
--- trunk/Open-ILS/web/opac/skin/default/js/holds.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/web/opac/skin/default/js/holds.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -16,7 +16,8 @@
     T : 'record',
     V : 'volume',
     I : 'issuance',
-    C : 'copy'
+    C : 'copy',
+    P : 'part'
 };
 
 
@@ -228,8 +229,11 @@
         } else if( type == 'I' ) {
             _h_set_issuance(args, doneCallback);
 
+        } else if( type == 'P' ) {
+            _h_set_parts(args, doneCallback);
+
 		} else {
-			if( type == 'T' ) {
+			if( type == 'T') {
 				_h_set_rec(args, doneCallback);
 			} else {
 				_h_set_rec_descriptors(args, doneCallback);
@@ -240,6 +244,25 @@
 	return args;
 }
 
+function _h_set_parts(args, doneCallback) {
+
+    var preq = new Request(
+        'open-ils.fielder:open-ils.fielder.bmp.atomic',
+        {"cache":1, "fields":["label", "record"],"query": {"id":args.part}}
+    );
+
+    preq.callback(
+        function(r) {
+            var part = r.getResultObject()[0];
+            args.record = part.record;
+            args.partObject = part;
+            _h_set_rec(args, doneCallback);
+        }
+    );
+
+    preq.send();
+}
+
 function _h_set_vol(args, doneCallback) {
 
 	if( args.volumeObject ) {
@@ -291,10 +314,13 @@
 	else 
 		args.recordObject = findRecord( args.record, 'T' );
 	
-	if( args.type == 'T' || args.type == 'M' ) 
+	if( args.type == 'T' || args.type == 'M' )  {
 		_h_set_rec_descriptors(args, doneCallback);
-	else 
+	//} else if(args.type == 'P') {
+        //_h_get_parts(args, doneCallback);
+    } else {
 		if(doneCallback) doneCallback(args);
+    }
 }
 
 
@@ -304,7 +330,7 @@
         args.pickup_lib = getSelectorVal($('holds_org_selector'));
 
     if(args.pickup_lib === null)
-        args.pickup_lib = holdArgs.recipient.home_ou();
+        args.pickup_lib = args.recipient.home_ou();
 
 	// grab the list of record desciptors attached to this records metarecord 
 	if( ! args.recordDescriptors )  {
@@ -332,25 +358,49 @@
 		req.callback(
 			function(r) {
 				var data = r.getResultObject();
-				holdArgs.recordDescriptors = args.recordDescriptors = data.descriptors;
-				holdArgs.metarecord = args.metarecord = data.metarecord;
+				args.recordDescriptors = args.recordDescriptors = data.descriptors;
+				args.metarecord = args.metarecord = data.metarecord;
 				if( args.type == 'M' && ! args.metarecordObject) 
-					holdArgs.metarecordObject = args.metarecordObject = findRecord(args.metarecord, 'M');	
+					args.metarecordObject = args.metarecordObject = findRecord(args.metarecord, 'M');	
 
-				if(doneCallback) doneCallback(args);
+                _h_get_parts(args, doneCallback);
 			}
 		);
 		req.send();
 
 	} else {
-		if(doneCallback) doneCallback(args);
+        _h_get_parts(args, doneCallback);
 	}
 
 	return args;
 }
 
+function _h_get_parts(args, doneCallback) {
 
+    if(args.type == 'M' || args.editHold || args.holdParts) {
+        if(doneCallback) 
+            doneCallback(args);
 
+    } else {
+
+		var req = new Request(
+            'open-ils.search:open-ils.search.biblio.record_hold_parts', 
+		    {pickup_lib: args.pickup_lib, record: args.record}
+        );
+
+		req.callback(
+			function(r) {
+				args.recordParts = r.getResultObject();
+                if(doneCallback)
+                    doneCallback(args);
+			}
+		);
+		req.send();
+    }
+}
+
+
+
 function holdsDrawWindow() {
 	swapCanvas($('holds_box'));
 	$('holds_cancel').onclick = function(){ runEvt('common', 'holdUpdateCanceled'), showCanvas() };
@@ -453,6 +503,30 @@
 		hideMe($('holds_issuance_row'));
 	}
 
+    if(holdArgs.recordParts && holdArgs.recordParts.length) {
+        var selector = $('holds_parts_selector');
+        unHideMe($('holds_parts_row'));
+        unHideMe(selector);
+
+        var nodeList = [];
+        dojo.forEach(selector.options, 
+            function(node) { if(node.value != '') nodeList.push(node) } );
+
+        dojo.forEach(nodeList, function(node) { selector.removeChild(node); });
+
+        dojo.forEach(
+            holdArgs.recordParts, 
+            function(part) {
+                insertSelectorVal(selector, -1, part.label, part.id);
+            }
+        );
+
+    } else if(holdArgs.type == 'P') {
+        unHideMe($('holds_parts_row'));
+        unHideMe($('holds_parts_label'));
+	    appendClear( $('holds_parts_label'), text(holdArgs.partObject.label));
+    }
+
 	removeChildren($('holds_format'));
 
 	var mods_formats = rec.types_of_resource();
@@ -623,6 +697,20 @@
 		if(type=='M') opt.selected=true;
 		unHideMe(opt);
 	}
+
+    // If the user selects a format, P-type holds are no longer an option
+    // disable and reset the P-type form control
+    selector.onchange = function() {
+        var partsSel = $('holds_parts_selector');
+        for(var i = 0; i < selector.options.length; i++) {
+            if(selector.options[i].selected) {
+                partsSel.selectedIndex = 0; // none selected
+                partsSel.disabled = true;
+                return;
+            }
+        }
+        partsSel.disabled = false;
+    }
 }
 
 function findFormatSelectorOptByParts( sel, val ) {
@@ -747,7 +835,8 @@
 		hold_type : holdArgs.type,
 		patronid : holdArgs.recipient.id(),
 		depth : 0, 
-		pickup_lib : pickuplib 
+		pickup_lib : pickuplib,
+        partid : holdArgs.part
 	};
 
 	if(recurse) {
@@ -839,8 +928,16 @@
 	else
 		hold.email_notify(0);
 
+    var part = getSelectorVal($('holds_parts_selector'));
+    if(part) {
+        holdArgs.type = 'P';
+        holdArgs.part = part;
+    }
+
 	var target = holdArgs[holdTargetTypeMap[holdArgs.type]];
 
+    // a mono part is selected
+
 	hold.pickup_lib(org); 
 	//hold.request_lib(org); 
 	hold.requestor(holdArgs.requestor.id());

Modified: trunk/Open-ILS/web/opac/skin/default/js/myopac.js
===================================================================
--- trunk/Open-ILS/web/opac/skin/default/js/myopac.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/web/opac/skin/default/js/myopac.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -499,7 +499,7 @@
 function myOPACDrawHoldTitle(hold) {
 	var method;
 
-	if( hold.hold_type() == 'T' || hold.hold_type() == 'M' ) {
+	if( hold.hold_type() == 'T' || hold.hold_type() == 'M') {
 		if(hold.hold_type() == "M") method = FETCH_MRMODS;
 		if(hold.hold_type() == "T") method = FETCH_RMODS;
 		var req = new Request(method, hold.target());
@@ -521,7 +521,7 @@
 
 function _myOPACFleshHoldTitle(hold, holdObjects) {
 
-	var record = holdObjects.recordObject;
+	var record  = holdObjects.recordObject;
 	var volume	= holdObjects.volumeObject;
 	var copy	= holdObjects.copyObject;
 
@@ -539,6 +539,11 @@
 	buildTitleDetailLink(record, title_link);
 	buildSearchLink(STYPE_AUTHOR, record.author(), author_link);
 
+    if(hold.hold_type() == 'P') {
+        unHideMe($n(row, 'vol_copy'));
+        $n(row, 'part').appendChild(text(holdObjects.partObject.label));
+    }
+
 	if( volume ) {
 		$n(row, 'volume').appendChild(text(volume.label()));
 		unHideMe($n(row, 'vol_copy'));

Modified: trunk/Open-ILS/web/opac/skin/default/js/rresult.js
===================================================================
--- trunk/Open-ILS/web/opac/skin/default/js/rresult.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/web/opac/skin/default/js/rresult.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -279,6 +279,17 @@
 	runEvt("result", "preCollectRecords");
 	var x = 0;
 
+    // don't perform rdetail redirect if user was on rdetail and cliecked Back
+    if(findCurrentPage() == RRESULT && isXUL()) {
+        if(ids.length == 1 && !xulG.fromBack) {
+            var args = {};
+            args.page = RDETAIL;
+            args[PARAM_OFFSET] = 0;
+            args[PARAM_RID] = ids[0];
+            location.href = buildOPACLink(args);
+        }
+    }
+
 	if (!base) base = 0;
 	if( rresultIsPaged )  base = 0;
 

Modified: trunk/Open-ILS/web/opac/skin/default/xml/common/holds.xml
===================================================================
--- trunk/Open-ILS/web/opac/skin/default/xml/common/holds.xml	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/web/opac/skin/default/xml/common/holds.xml	2011-03-29 15:24:44 UTC (rev 19883)
@@ -51,6 +51,16 @@
 					<td class='holds_cell' id='holds_physical_desc'> </td>
 				</tr>
 
+				<tr class='hide_me' id='holds_parts_row'>
+					<td class='holds_cell'>&common.mono_parts.label;</td>
+					<td class='holds_cell'>
+                        <span class='hide_me' id='holds_parts_label'></span>
+                        <select id='holds_parts_selector' class='hide_me'>
+                            <option value=''></option>
+                        </select>
+                    </td>
+				</tr>
+
 				<tr class='hide_me' id='holds_cn_row'>
 					<td class='holds_cell'>&common.call.number.label;</td>
 					<td class='holds_cell'><b id='holds_cn'/> </td>

Modified: trunk/Open-ILS/web/opac/skin/default/xml/myopac/myopac_holds.xml
===================================================================
--- trunk/Open-ILS/web/opac/skin/default/xml/myopac/myopac_holds.xml	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/web/opac/skin/default/xml/myopac/myopac_holds.xml	2011-03-29 15:24:44 UTC (rev 19883)
@@ -71,8 +71,9 @@
 				<td name='myopac_holds_title'>
 					<a href='javascript:void(0);' name='myopac_holds_title_link'> </a>
 					<div name='vol_copy' style='border: 1px solid #808080; width:98%; margin-top: 2px;' class='hide_me'>
-						<div style='font-size: 90%' name='volume'/>
-						<div style='font-size: 90%' name='copy'/>
+						<div style='font-size: 90%' name='part'></div>
+						<div style='font-size: 90%' name='volume'></div>
+						<div style='font-size: 90%' name='copy'></div>
 					</div>
 				</td>
 

Modified: trunk/Open-ILS/web/opac/skin/default/xml/rdetail/rdetail_cn_details.xml
===================================================================
--- trunk/Open-ILS/web/opac/skin/default/xml/rdetail/rdetail_cn_details.xml	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/web/opac/skin/default/xml/rdetail/rdetail_cn_details.xml	2011-03-29 15:24:44 UTC (rev 19883)
@@ -13,6 +13,7 @@
 								<td width='33%'>&rdetail.cn.barcode;</td>
 								<td>&common.status;</td>
 								<td>&rdetail.cn.location;</td>
+								<td name='copy_part_label' class='hide_me'>&rdetail.cn.part;</td>
 								<td name='age_protect_label' class='hide_me'>&rdetail.cn.hold.age;</td>
 								<td name='create_date_label' class='hide_me'>&rdetail.cn.genesis;</td>
 								<td name='holdable_label' class='hide_me'>&rdetail.cn.holdable;</td>
@@ -35,6 +36,7 @@
 
 								<td name='status'> </td>
 								<td name='location'> </td>
+								<td name='copy_part' class='hide_me'> </td>
 								<td name='age_protect_value' class='hide_me'>&rdetail.cn.disabled;</td>
 								<td name='create_date_value' class='hide_me'> </td>
 

Added: trunk/Open-ILS/web/templates/default/conify/global/biblio/monograph_part.tt2
===================================================================
--- trunk/Open-ILS/web/templates/default/conify/global/biblio/monograph_part.tt2	                        (rev 0)
+++ trunk/Open-ILS/web/templates/default/conify/global/biblio/monograph_part.tt2	2011-03-29 15:24:44 UTC (rev 19883)
@@ -0,0 +1,37 @@
+[% WRAPPER default/base.tt2 %]
+[% ctx.page_title = 'Configure Monograph Parts' %]
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+    <div dojoType="dijit.layout.ContentPane" layoutAlign="top" class='oils-header-panel'>
+        <div>Monograph Parts</div>
+        <div>
+            <button dojoType='dijit.form.Button' onClick='monoPartGrid.showCreateDialog()'>New Monograph Part</button>
+            <button dojoType='dijit.form.Button' onClick='monoPartGrid.deleteSelected()'>Delete Selected</button>
+        </div>
+    </div>
+    <div>
+    <table  jsId="monoPartGrid"
+            dojoType="openils.widget.AutoGrid"
+            autoHeight='true'
+            fieldOrder="['label']"
+            suppressFields="['id','record','label_sortkey']"
+            suppressEditFields="['id','label_sortkey']"
+            query="{id: null}"
+            fmClass='bmp'
+            editOnEnter='true'/>
+</div>
+
+<script type="text/javascript">
+    dojo.require('openils.CGI');
+    dojo.require('openils.Util');
+    dojo.require('openils.widget.AutoGrid');
+
+    var cgi = new openils.CGI();
+    openils.Util.addOnLoad( function() {
+        monoPartGrid.overrideEditWidgets.record = new dijit.form.TextBox({"disabled": true});
+        monoPartGrid.overrideEditWidgets.record.shove = { create : cgi.param('r') };
+        monoPartGrid.loadAll({order_by : {bmp : 'label'}}, {record : cgi.param('r')});
+    });
+</script>
+[% END %]
+
+

Added: trunk/Open-ILS/web/templates/default/conify/global/config/acn_prefix.tt2
===================================================================
--- trunk/Open-ILS/web/templates/default/conify/global/config/acn_prefix.tt2	                        (rev 0)
+++ trunk/Open-ILS/web/templates/default/conify/global/config/acn_prefix.tt2	2011-03-29 15:24:44 UTC (rev 19883)
@@ -0,0 +1,37 @@
+[% WRAPPER default/base.tt2 %]
+[% ctx.page_title = 'Call Number Prefixes' %]
+<script type="text/javascript" src='[% ctx.media_prefix %]/js/ui/default/conify/global/config/acn_prefix.js'> </script>
+
+<!-- grid -->
+
+ <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+        <div dojoType="dijit.layout.ContentPane" layoutAlign="top" class='oils-header-panel'>
+            <div>Call Number Prefixes</div>
+            <div>
+                <button dojoType='dijit.form.Button' onClick='thingGrid.showCreateDialog()'>New Prefix</button>
+                <button dojoType='dijit.form.Button' onClick='thingGrid.deleteSelected()'>Delete Selected</button>
+            </div>
+        </div>
+        <div>
+            <span>Context Org Unit</span>
+            <select dojoType="openils.widget.OrgUnitFilteringSelect" jsId='thingContextOrgSelect'
+                searchAttr='shortname' labelAttr='shortname'> </select>
+        </div>
+        <table  jsId="thingGrid"
+                dojoType="openils.widget.AutoGrid"
+                fieldOrder="['id', 'label', 'owning_lib']"
+                suppressFields="['label_sortkey']"
+                suppressEditFields="['label_sortkey']"
+                query="{id: '*'}"
+                defaultCellWidth='20'
+                fmClass='acnp'
+                editOnEnter='true'>
+            <thead>
+                <tr><th field='owning_lib' get='getOrgInfo'/></tr>
+            </thead>
+        </table>
+    </div>
+</div>
+[% END %]
+
+

Added: trunk/Open-ILS/web/templates/default/conify/global/config/acn_suffix.tt2
===================================================================
--- trunk/Open-ILS/web/templates/default/conify/global/config/acn_suffix.tt2	                        (rev 0)
+++ trunk/Open-ILS/web/templates/default/conify/global/config/acn_suffix.tt2	2011-03-29 15:24:44 UTC (rev 19883)
@@ -0,0 +1,37 @@
+[% WRAPPER default/base.tt2 %]
+[% ctx.page_title = 'Call Number Suffixes' %]
+<script type="text/javascript" src='[% ctx.media_prefix %]/js/ui/default/conify/global/config/acn_suffix.js'> </script>
+
+<!-- grid -->
+
+ <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+        <div dojoType="dijit.layout.ContentPane" layoutAlign="top" class='oils-header-panel'>
+            <div>Call Number Suffixes</div>
+            <div>
+                <button dojoType='dijit.form.Button' onClick='thingGrid.showCreateDialog()'>New Suffix</button>
+                <button dojoType='dijit.form.Button' onClick='thingGrid.deleteSelected()'>Delete Selected</button>
+            </div>
+        </div>
+        <div>
+            <span>Context Org Unit</span>
+            <select dojoType="openils.widget.OrgUnitFilteringSelect" jsId='thingContextOrgSelect'
+                searchAttr='shortname' labelAttr='shortname'> </select>
+        </div>
+        <table  jsId="thingGrid"
+                dojoType="openils.widget.AutoGrid"
+                fieldOrder="['id', 'label', 'owning_lib']"
+                suppressFields="['label_sortkey']"
+                suppressEditFields="['label_sortkey']"
+                query="{id: '*'}"
+                defaultCellWidth='20'
+                fmClass='acns'
+                editOnEnter='true'>
+            <thead>
+                <tr><th field='owning_lib' get='getOrgInfo'/></tr>
+            </thead>
+        </table>
+    </div>
+</div>
+[% END %]
+
+

Modified: trunk/Open-ILS/xul/staff_client/chrome/content/OpenILS/data.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/chrome/content/OpenILS/data.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/chrome/content/OpenILS/data.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -534,6 +534,9 @@
             }
         }
 
+        // If we don't clear these, then things like obj.list['acnp_for_lib_1'] may stick around
+        obj.hash = {}; obj.list = {};
+
         this.chain = [];
 
         this.chain.push(
@@ -598,6 +601,27 @@
         this.chain.push(
             function() {
                 var f = gen_fm_retrieval_func(
+                    'acnc',
+                    [
+                        api.FM_ACNC_RETRIEVE_VIA_PCRUD.app,
+                        api.FM_ACNC_RETRIEVE_VIA_PCRUD.method,
+                        [ obj.session.key, {"id":{"!=":null}}, {"order_by":{"acnc":"name"}} ],
+                        false
+                    ]
+                );
+                try {
+                    f();
+                } catch(E) {
+                    var error = 'Error: ' + js2JSON(E);
+                    obj.error.sdump('D_ERROR',error);
+                    throw(E);
+                }
+            }
+        );
+
+        this.chain.push(
+            function() {
+                var f = gen_fm_retrieval_func(
                     'ahrcc',
                     [
                         api.FM_AHRCC_PCRUD_SEARCH.app,
@@ -827,7 +851,6 @@
             }
         );
 
-
         this.chain.push(
             function() {
                 var f = gen_fm_retrieval_func(
@@ -852,6 +875,50 @@
         this.chain.push(
             function() {
                 var f = gen_fm_retrieval_func(
+                    'acnp',
+                    [
+                        api.FM_ACNP_RETRIEVE_VIA_PCRUD.app,
+                        api.FM_ACNP_RETRIEVE_VIA_PCRUD.method,
+                        [ obj.session.key, {"owning_lib":{"=":obj.list.au[0].ws_ou()}}, {"order_by":{"acnp":"label_sortkey"}} ],
+                        false
+                    ]
+                );
+                try {
+                    f();
+                    obj.list['acnp_for_lib_'+obj.list.au[0].ws_ou()] = obj.list.acnp;
+                } catch(E) {
+                    var error = 'Error: ' + js2JSON(E);
+                    obj.error.sdump('D_ERROR',error);
+                    throw(E);
+                }
+            }
+        );
+
+        this.chain.push(
+            function() {
+                var f = gen_fm_retrieval_func(
+                    'acns',
+                    [
+                        api.FM_ACNS_RETRIEVE_VIA_PCRUD.app,
+                        api.FM_ACNS_RETRIEVE_VIA_PCRUD.method,
+                        [ obj.session.key, {"owning_lib":{"=":obj.list.au[0].ws_ou()}}, {"order_by":{"acns":"label_sortkey"}} ],
+                        false
+                    ]
+                );
+                try {
+                    f();
+                    obj.list['acns_for_lib_'+obj.list.au[0].ws_ou()] = obj.list.acns;
+                } catch(E) {
+                    var error = 'Error: ' + js2JSON(E);
+                    obj.error.sdump('D_ERROR',error);
+                    throw(E);
+                }
+            }
+        );
+
+        this.chain.push(
+            function() {
+                var f = gen_fm_retrieval_func(
                     'cbt',
                     [
                         api.FM_CBT_RETRIEVE.app,

Modified: trunk/Open-ILS/xul/staff_client/chrome/content/OpenILS/global_util.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/chrome/content/OpenILS/global_util.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/chrome/content/OpenILS/global_util.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -413,6 +413,12 @@
 
     function update_modal_xulG(v) {
         try {
+            if (typeof xulG != "undefined" && xulG.not_modal) {
+                xulG = v;
+                xulG.not_modal = true;
+                return;
+            }
+
             JSAN.use('OpenILS.data'); var data = new OpenILS.data(); data.init({'via':'stash'});
             var key = location.pathname + location.search + location.hash;
             if (typeof data.modal_xulG_stack != 'undefined' && typeof data.modal_xulG_stack[key] != 'undefined') {
@@ -453,11 +459,15 @@
             }
             if (typeof _params.no_xulG == 'undefined') {
                 if (typeof _params.modal_xulG != 'undefined') {
-                    JSAN.use('OpenILS.data'); var data = new OpenILS.data(); data.init({'via':'stash'});
-                    var key = location.pathname + location.search + location.hash;
-                    //dump('xul_param, considering modal key = ' + key + '\n');
-                    if (typeof data.modal_xulG_stack != 'undefined' && typeof data.modal_xulG_stack[key] != 'undefined') {
-                        xulG = data.modal_xulG_stack[key][ data.modal_xulG_stack[key].length - 1 ];
+                    if (typeof xulG != 'undefined' && xulG.not_modal) {
+                        // for interfaces that used to be modal but aren't now, do nothing
+                    } else {
+                        JSAN.use('OpenILS.data'); var data = new OpenILS.data(); data.init({'via':'stash'});
+                        var key = location.pathname + location.search + location.hash;
+                        //dump('xul_param, considering modal key = ' + key + '\n');
+                        if (typeof data.modal_xulG_stack != 'undefined' && typeof data.modal_xulG_stack[key] != 'undefined') {
+                            xulG = data.modal_xulG_stack[key][ data.modal_xulG_stack[key].length - 1 ];
+                        }
                     }
                 }
                 if (typeof xulG == 'object' && typeof xulG[ param_name ] != 'undefined') {

Modified: trunk/Open-ILS/xul/staff_client/chrome/content/cat/opac.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/chrome/content/cat/opac.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/chrome/content/cat/opac.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -89,18 +89,27 @@
 function set_brief_view() {
     var url = xulG.url_prefix( urls.XUL_BIB_BRIEF ) + '?docid=' + window.escape(docid); 
     dump('spawning ' + url + '\n');
+
+    var content_params = {
+        'set_tab_name' : function(n) {
+            if (typeof window.xulG == 'object' && typeof window.xulG.set_tab_name == 'function') {
+                try { window.xulG.set_tab_name(document.getElementById('offlineStrings').getFormattedString("cat.bib_record", [n])); } catch(E) { alert(E); }
+            } else {
+                dump('no set_tab_name\n');
+            }
+        }
+    };
+
+    ["url_prefix", "new_tab", "set_tab", "close_tab", "new_patron_tab",
+        "set_patron_tab", "volume_item_creator", "get_new_session",
+        "holdings_maintenance_tab", "open_chrome_window", "url_prefix",
+        "network_meter", "page_meter", "set_statusbar", "set_help_context"
+    ].forEach(function(k) { content_params[k] = xulG[k]; });
+
     top_pane.set_iframe( 
         url,
-        {}, 
-        { 
-            'set_tab_name' : function(n) { 
-                if (typeof window.xulG == 'object' && typeof window.xulG.set_tab_name == 'function') {
-                    try { window.xulG.set_tab_name(document.getElementById('offlineStrings').getFormattedString("cat.bib_record", [n])); } catch(E) { alert(E); }
-                } else {
-                    dump('no set_tab_name\n');
-                }
-            }
-        }  
+        {},
+        content_params
     );
 }
 
@@ -172,13 +181,13 @@
                             JSAN.use('util.error'); error = new util.error();
                             JSAN.use('util.network'); var network = new util.network();
 
-                            var acn_id = network.simple_request(
+                            var acn_blob = network.simple_request(
                                 'FM_ACN_FIND_OR_CREATE',
                                 [ ses(), cn_label, doc_id, ses('ws_ou') ]
                             );
 
-                            if (typeof acn_id.ilsevent != 'undefined') {
-                                error.standard_unexpected_error_alert('Error in chrome/content/cat/opac.js, cat.util.fast_item_add', acn_id);
+                            if (typeof acn_blob.ilsevent != 'undefined') {
+                                error.standard_unexpected_error_alert('Error in chrome/content/cat/opac.js, cat.util.fast_item_add', acn_blob);
                                 return;
                             }
 
@@ -186,7 +195,7 @@
                             copy_obj.id( -1 );
                             copy_obj.isnew('1');
                             copy_obj.barcode( cp_barcode );
-                            copy_obj.call_number( acn_id );
+                            copy_obj.call_number( acn_blob.acn_id );
                             copy_obj.circ_lib( ses('ws_ou') );
                             /* FIXME -- use constants */
                             copy_obj.deposit(0);
@@ -822,8 +831,10 @@
 
         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 w = xulG.new_tab(
-            window.xulG.url_prefix(urls.XUL_VOLUME_COPY_CREATOR),
+            url,
             { 'tab_name' : title },
             { 'doc_id' : docid, 'ou_ids' : [ ses('ws_ou') ] }
         );
@@ -831,3 +842,20 @@
         alert('Error in chrome/content/cat/opac.js, add_volumes(): ' + E);
     }
 }
+
+function manage_parts() {
+    try {
+        var title = document.getElementById('offlineStrings').getFormattedString('staff.cat.manage_parts.title', [docid]);
+        var loc = urls.XUL_BROWSER + "?url=" + window.escape(
+            window.xulG.url_prefix(urls.CONIFY_MANAGE_PARTS) + '?r=' + docid
+        );
+        var w = xulG.new_tab(
+            loc,
+            { 'tab_name' : title },
+            {}
+        );
+    } catch(E) {
+        alert('Error in chrome/content/cat/opac.js, manage_parts(): ' + E);
+    }
+}
+

Modified: trunk/Open-ILS/xul/staff_client/chrome/content/cat/opac.xul
===================================================================
--- trunk/Open-ILS/xul/staff_client/chrome/content/cat/opac.xul	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/chrome/content/cat/opac.xul	2011-03-29 15:24:44 UTC (rev 19883)
@@ -59,6 +59,7 @@
                 <menuitem label="&staff.cat.copy_browser.holdings_maintenance.cmd_add_volumes.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.cmd_add_volumes.accesskey;" id="add_volumes" oncommand="add_volumes();"/>
                 <menuitem label="&staff.cat.opac.mark_for_hold_transfer.label;" accesskey="&staff.cat.opac.mark_for_hold_transfer.accesskey;" id="mark_for_hold_transfer" oncommand="mark_for_hold_transfer();"/>
                 <menuitem label="&staff.cat.opac.transfer_title_holds.label;" accesskey="&staff.cat.opac.transfer_title_holds.accesskey;" id="transfer_title_holds" oncommand="transfer_title_holds();"/>
+                <menuitem label="&staff.cat.opac.manage_parts.label;" accesskey="&staff.cat.opac.manage_parts.accesskey;" id="manage_parts" oncommand="manage_parts();"/>
                 <menuseparator/>
                 <menuitem label="&staff.cat.opac.bib_in_new_tab.label;" id="bib_in_new_tab" oncommand="bib_in_new_tab();"/>
                 <menuitem label="&staff.cat.opac.remove_me.label;" id="remove_me" oncommand="remove_me();"/>

Modified: trunk/Open-ILS/xul/staff_client/chrome/content/main/constants.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/chrome/content/main/constants.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/chrome/content/main/constants.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -81,13 +81,16 @@
     'CHECKOUT_RENEW' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.renew' },
     'CIRC_MODIFIER_LIST' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.circ_modifier.retrieve.all' },
     'CLEAR_HOLD_SHELF' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.hold.clear_shelf.process', 'secure' : false },
-    'FM_ACN_RETRIEVE' : { 'app' : 'open-ils.search', 'method' : 'open-ils.search.callnumber.retrieve', 'secure' : false },
-    'FM_ACN_RETRIEVE.authoritative' : { 'app' : 'open-ils.search', 'method' : 'open-ils.search.callnumber.retrieve.authoritative', 'secure' : false },
+    'FM_ACN_RETRIEVE' : { 'app' : 'open-ils.search', 'method' : 'open-ils.search.callnumber.fleshed.retrieve', 'secure' : false },
+    'FM_ACN_RETRIEVE.authoritative' : { 'app' : 'open-ils.search', 'method' : 'open-ils.search.callnumber.fleshed.retrieve.authoritative', 'secure' : false },
     'FM_ACN_TREE_UPDATE' : { 'app' : 'open-ils.cat', 'method' : 'open-ils.cat.asset.volume.fleshed.batch.update' },
     'FM_ACN_TREE_LIST_RETRIEVE_VIA_RECORD_ID_AND_ORG_IDS' : { 'app' : 'open-ils.cat', 'method' : 'open-ils.cat.asset.copy_tree.retrieve', 'secure' : false },
     'FM_ACN_TREE_LIST_RETRIEVE_VIA_RECORD_ID_AND_ORG_IDS.authoritative' : { 'app' : 'open-ils.cat', 'method' : 'open-ils.cat.asset.copy_tree.retrieve.authoritative', 'secure' : false },
     'FM_ACN_TRANSFER' : { 'app' : 'open-ils.cat', 'method' : 'open-ils.cat.asset.volume.batch.transfer' },
-    'FM_ACN_FIND_OR_CREATE' : { 'app' : 'open-ils.cat', 'method' : 'open-ils.cat.call_number.find_or_create', 'secure' : false },
+    'FM_ACN_FIND_OR_CREATE' : { 'app' : 'open-ils.cat', 'method' : 'open-ils.cat.call_number.find_or_create' },
+    'FM_ACNC_RETRIEVE_VIA_PCRUD' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.search.acnc.atomic' },
+    'FM_ACNP_RETRIEVE_VIA_PCRUD' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.search.acnp.atomic' },
+    'FM_ACNS_RETRIEVE_VIA_PCRUD' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.search.acns.atomic' },
     'FM_ACP_DETAILS' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.copy_details.retrieve' },
     'FM_ACP_DETAILS_VIA_BARCODE' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.copy_details.retrieve.barcode' },
     'FM_ACP_DETAILS_VIA_BARCODE.authoritative' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.copy_details.retrieve.barcode.authoritative' },
@@ -471,7 +474,9 @@
     'XUL_USER_BUCKETS' : '/xul/server/patron/user_buckets.xul',
     'XUL_VERIFY_CREDENTIALS' : '/xul/server/main/verify_credentials.xul',
     'XUL_VOLUME_BUCKETS' : '/xul/server/cat/volume_buckets.xul',
-    'XUL_VOLUME_COPY_CREATOR' : '/xul/server/cat/volume_copy_creator.xul',
+    'XUL_VOLUME_COPY_CREATOR' : '/xul/server/cat/volume_copy_editor.xul',
+    'XUL_VOLUME_COPY_CREATOR_HORIZONTAL' : '/xul/server/cat/volume_copy_editor_horiz.xul',
+    'XUL_VOLUME_COPY_CREATOR_ORIGINAL' : '/xul/server/cat/volume_copy_creator.xul',
     'XUL_VOLUME_EDITOR' : '/xul/server/cat/volume_editor.xul',
     'XUL_WORK_LOG' : '/xul/server/admin/work_log.xul',
     'XUL_WORKSTATION_INFO' : '/xul/server/main/ws_info.xul',
@@ -479,6 +484,7 @@
     'TEST_HTML' : '/xul/server/main/test.html',
     'TEST_XUL' : '/xul/server/main/test.xul',
     'CONIFY' : '/conify/' + LOCALE + '/global',
+    'CONIFY_MANAGE_PARTS' : '/eg/conify/global/biblio/monograph_part',
     'EG_WEB_BASE' : '/eg',
     'XUL_LOCAL_ADMIN_BASE' : '/xul/server/admin',
     'XUL_REPORTS' : '/reports/oils_rpt.xhtml',

Modified: trunk/Open-ILS/xul/staff_client/chrome/content/main/menu.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/chrome/content/main/menu.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/chrome/content/main/menu.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -732,10 +732,14 @@
                 ['oncommand'],
                 function() { open_eg_web_page('conify/global/config/record_attr_definition'); }
             ],
-            'cmd_server_admin_coded_value_map' : [
+            'cmd_server_admin_acn_prefix' : [
                 ['oncommand'],
-                function() { open_eg_web_page('conify/global/config/coded_value_map'); }
+                function() { open_eg_web_page('conify/global/config/acn_prefix'); }
             ],
+            'cmd_server_admin_acn_suffix' : [
+                ['oncommand'],
+                function() { open_eg_web_page('conify/global/config/acn_suffix'); }
+            ],
             'cmd_server_admin_billing_type' : [
                 ['oncommand'],
                 function() { open_eg_web_page('conify/global/config/billing_type'); }
@@ -1588,8 +1592,10 @@
     },
     '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 w = obj.new_tab(
-            obj.url_prefix(urls.XUL_VOLUME_COPY_CREATOR),
+            url,
             { 'tab_name' : document.getElementById('offlineStrings').getString('staff.cat.create_or_rebarcode_items') },
             params
         );

Modified: trunk/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
===================================================================
--- trunk/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul	2011-03-29 15:24:44 UTC (rev 19883)
@@ -141,6 +141,8 @@
     <command id="cmd_server_admin_marc_code"/>
     <command id="cmd_server_admin_coded_value_map"/>
     <command id="cmd_server_admin_billing_type"/>
+    <command id="cmd_server_admin_acn_prefix"/>
+    <command id="cmd_server_admin_acn_suffix"/>
     <command id="cmd_server_admin_acq_invoice_item_type"/>
     <command id="cmd_server_admin_acq_invoice_payment_method"/>
     <command id="cmd_server_admin_acq_cancel_reason"/>
@@ -392,6 +394,8 @@
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.grp_tree.label;" command="cmd_server_admin_grp_tree"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.perm_list.label;" command="cmd_server_admin_perm_list"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.copy_status.label;" command="cmd_server_admin_copy_status"/>
+                <menuitem label="&staff.main.menu.admin.server_admin.conify.acn_prefix.label;" command="cmd_server_admin_acn_prefix"/>
+                <menuitem label="&staff.main.menu.admin.server_admin.conify.acn_suffix.label;" command="cmd_server_admin_acn_suffix"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.marc_record_attrs.label;" command="cmd_server_admin_marc_code"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.coded_value_maps.label;" command="cmd_server_admin_coded_value_map"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.billing_type.label;" command="cmd_server_admin_billing_type"/>

Modified: trunk/Open-ILS/xul/staff_client/chrome/content/util/browser.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/chrome/content/util/browser.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/chrome/content/util/browser.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -14,6 +14,9 @@
 
     'lock_reload' : false, // as opposed to lock 'n load :)
 
+    'back_button_clicked' : false,
+    'from_back' : false,
+
     'init' : function( params ) {
 
         try {
@@ -115,7 +118,10 @@
                                 try {
                                     netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
                                     var n = obj.getWebNavigation();
-                                    if (n.canGoBack) n.goBack();
+                                    if (n.canGoBack) {
+                                        obj.back_button_clicked = true;
+                                        n.goBack();
+                                    }
                                 } catch(E) {
                                     var err = 'cmd_back: ' + E;
                                     obj.error.sdump('D_ERROR',err);
@@ -265,6 +271,7 @@
             cw.IAMXUL = true;
             cw.XUL_BUILD_ID = '/xul/server/'.split(/\//)[2];
             cw.xulG = obj.passthru_content_params || {};
+            cw.xulG.fromBack = obj.from_back;
             if (!cw.xulG.set_tab) { cw.xulG.set_tab = function(a,b,c) { return window.xulG.set_tab(a,b,c); }; }
             if (!cw.xulG.new_tab) { cw.xulG.new_tab = function(a,b,c) { return window.xulG.new_tab(a,b,c); }; }
             if (!cw.xulG.close_tab) { cw.xulG.close_tab = function(a) { return window.xulG.close_tab(a); }; }
@@ -412,6 +419,7 @@
                         if (stateFlags & nsIWebProgressListener.STATE_IS_DOCUMENT) {
                             s += ('\tSTATE_IS_DOCUMENT\n');
                             if( stateFlags & nsIWebProgressListener.STATE_STOP ) {
+                                var alert_string = 'document has stopped: ' + new Date() + '\n'; dump(alert_string);
                                 obj.push_variables(); obj.updateNavButtons();
                                 if (typeof obj.on_url_load == 'function') {
                                     try {
@@ -448,6 +456,8 @@
                         }
                         if (stateFlags & nsIWebProgressListener.STATE_START) {
                             s += ('\tSTATE_START\n');
+                            obj.from_back = obj.back_button_clicked;
+                            obj.back_button_clicked = false;
                         }
                         if (stateFlags & nsIWebProgressListener.STATE_REDIRECTING) {
                             s += ('\tSTATE_REDIRECTING\n');

Modified: trunk/Open-ILS/xul/staff_client/chrome/content/util/list.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/chrome/content/util/list.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/chrome/content/util/list.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -30,6 +30,7 @@
     'init' : function (params) {
 
         var obj = this;
+        obj.scratch_data = {};
 
         JSAN.use('util.widgets');
 
@@ -1016,11 +1017,11 @@
     
                 if (typeof params.map_row_to_column == 'function')  {
     
-                    label = params.map_row_to_column(params.row,this.columns[i]);
+                    label = params.map_row_to_column(params.row,this.columns[i],this.scratch_data);
     
                 } else if (typeof this.map_row_to_column == 'function') {
     
-                    label = this.map_row_to_column(params.row,this.columns[i]);
+                    label = this.map_row_to_column(params.row,this.columns[i],this.scratch_data);
     
                 }
                 if (this.columns[i].type == 'checkbox') { treecell.setAttribute('value',label); } else { treecell.setAttribute('label',label ? label : ''); }
@@ -1032,11 +1033,11 @@
 
             if (typeof params.map_row_to_columns == 'function') {
 
-                labels = params.map_row_to_columns(params.row,this.columns);
+                labels = params.map_row_to_columns(params.row,this.columns,this.scratch_data);
 
             } else if (typeof this.map_row_to_columns == 'function') {
 
-                labels = this.map_row_to_columns(params.row,this.columns);
+                labels = this.map_row_to_columns(params.row,this.columns,this.scratch_data);
 
             }
             for (var i = 0; i < labels.length; i++) {
@@ -1070,13 +1071,13 @@
             var value = '';
             if (typeof params.map_row_to_column == 'function')  {
 
-                value = params.map_row_to_column(params.row,this.columns[i]);
+                value = params.map_row_to_column(params.row,this.columns[i],this.scratch_data);
 
             } else {
 
                 if (typeof this.map_row_to_column == 'function') {
 
-                    value = this.map_row_to_column(params.row,this.columns[i]);
+                    value = this.map_row_to_column(params.row,this.columns[i],this.scratch_data);
                 }
             }
             if (typeof value == 'string' || typeof value == 'number') {
@@ -1817,9 +1818,11 @@
     },
     // Default for the map_row_to_columns function for .init
     'std_map_row_to_columns' : function(error_value) {
-        return function(row,cols) {
+        return function(row,cols,scratch) {
             // row contains { 'my' : { 'acp' : {}, 'circ' : {}, 'mvr' : {} } }
             // cols contains all of the objects listed above in columns
+            // scratch is a temporary space shared by all cells/rows (or just per row if not explicitly passed in)
+            if (!scratch) { scratch = {}; }
 
             var obj = {};
             JSAN.use('util.error'); obj.error = new util.error();
@@ -1833,7 +1836,7 @@
             try {
                 for (var i = 0; i < cols.length; i++) {
                     switch (typeof cols[i].render) {
-                        case 'function': try { values[i] = cols[i].render(my); } catch(E) { values[i] = error_value; obj.error.sdump('D_COLUMN_RENDER_ERROR',E); } break;
+                        case 'function': try { values[i] = cols[i].render(my,scratch); } catch(E) { values[i] = error_value; obj.error.sdump('D_COLUMN_RENDER_ERROR',E); } break;
                         case 'string' : cmd += 'try { ' + cols[i].render + '; values['+i+'] = v; } catch(E) { values['+i+'] = error_value; }'; break;
                         default: cmd += 'values['+i+'] = "??? '+(typeof cols[i].render)+'"; ';
                     }

Modified: trunk/Open-ILS/xul/staff_client/chrome/content/util/widgets.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/chrome/content/util/widgets.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/chrome/content/util/widgets.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -134,7 +134,9 @@
             menuitem.setAttribute('disabled','true');
         }
     }
-    menulist.setAttribute('value',dvalue);
+    if (typeof dvalue != 'undefined') {
+        menulist.setAttribute('value',dvalue);
+    }
     return menulist;
 }
 
@@ -209,7 +211,7 @@
     }
 }
 
-util.widgets.apply_vertical_tab_on_enter_handler = function(node,onfailure) {
+util.widgets.apply_vertical_tab_on_enter_handler = function(node,onfailure,no_enter_func) {
     try {
         node.addEventListener(
             'keypress',
@@ -224,9 +226,14 @@
                         ev.preventDefault(); ev.stopPropagation();
                         return true;
                     } else {
+                        dump('keypress: attempting onfailure\n');
                         if (typeof onfailure == 'function') return onfailure(ev);
                         return false;
                     }
+                } else {
+                    if (typeof no_enter_func == 'function') {
+                        no_enter_func(ev);
+                    }
                 }
             },
             false

Modified: trunk/Open-ILS/xul/staff_client/chrome/locale/en-US/offline.properties
===================================================================
--- trunk/Open-ILS/xul/staff_client/chrome/locale/en-US/offline.properties	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/chrome/locale/en-US/offline.properties	2011-03-29 15:24:44 UTC (rev 19883)
@@ -256,6 +256,7 @@
 staff.cat.util.copy_editor.view=View
 staff.circ.copy_status.add_volumes.perm_failure=You do not have permission to add volumes to the workstation library.
 staff.circ.copy_status.add_volumes.title=Add Volume/Item for Record # %1$s
+staff.cat.manage_parts.title=Manage Parts for Record # %1$s
 staff.cat.z3950.marked_record_for_overlay_indicator.tcn.label=Record with TCN %1$s marked for overlay.
 staff.cat.z3950.marked_record_for_overlay_indicator.record_id.label=Record with ID %1$s marked for overlay.
 staff.cat.opac.marked_record_for_hold_transfer_indicator.tcn.label=Record with TCN %1$s marked for title hold transfer.

Modified: trunk/Open-ILS/xul/staff_client/server/cat/bib_brief.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/cat/bib_brief.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/server/cat/bib_brief.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -9,13 +9,13 @@
         JSAN.use('util.error'); g.error = new util.error();
         g.error.sdump('D_TRACE','my_init() for cat_bib_brief.xul');
 
-        JSAN.use('OpenILS.data'); var data = new OpenILS.data(); data.init({'via':'stash'});
+        JSAN.use('OpenILS.data'); g.data = new OpenILS.data(); g.data.init({'via':'stash'});
 
         docid = xul_param('docid');
 
         var key = location.pathname + location.search + location.hash;
-        if (!docid && typeof data.modal_xulG_stack != 'undefined' && typeof data.modal_xulG_stack[key] != 'undefined') {
-            var xulG = data.modal_xulG_stack[key][ data.modal_xulG_stack[key].length - 1 ];
+        if (!docid && typeof g.data.modal_xulG_stack != 'undefined' && typeof g.data.modal_xulG_stack[key] != 'undefined') {
+            var xulG = g.data.modal_xulG_stack[key][ g.data.modal_xulG_stack[key].length - 1 ];
             if (typeof xulG == 'object') {
                 docid = xulG.docid;
             }
@@ -28,7 +28,7 @@
 
         if (docid > -1) {
 
-            data.last_record = docid; data.stash('last_record');
+            g.data.last_record = docid; g.data.stash('last_record');
 
             g.network.simple_request(
                 'MODS_SLIM_RECORD_RETRIEVE.authoritative',
@@ -87,6 +87,14 @@
     }
 }
 
+function unhide_add_volumes_button() {
+    if (xulG && typeof xulG == 'object' && typeof xulG['new_tab'] == 'function') {
+        document.getElementById('add_volumes').hidden = false;
+        document.getElementById('add_volumes_left_paren').hidden = false;
+        document.getElementById('add_volumes_right_paren').hidden = false;
+    }
+}
+
 function view_marc() {
     try {
         JSAN.use('util.window'); var win = new util.window();
@@ -114,4 +122,39 @@
     }
 }
 
+function add_volumes() {
+    try {
+        var edit = 0;
+        try {
+            edit = g.network.request(
+                api.PERM_MULTI_ORG_CHECK.app,
+                api.PERM_MULTI_ORG_CHECK.method,
+                [
+                    ses(),
+                    ses('staff_id'),
+                    [ ses('ws_ou') ],
+                    [ 'CREATE_VOLUME', 'CREATE_COPY' ]
+                ]
+            ).length == 0 ? 1 : 0;
+        } catch(E) {
+            g.error.sdump('D_ERROR','batch permission check: ' + E);
+        }
 
+        if (edit==0) {
+            alert(document.getElementById('offlineStrings').getString('staff.circ.copy_status.add_volumes.perm_failure'));
+            return; // no read-only view for this interface
+        }
+
+        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 w = xulG.new_tab(
+            url,
+            { 'tab_name' : title },
+            { 'doc_id' : docid, 'ou_ids' : [ ses('ws_ou') ] }
+        );
+    } catch(E) {
+        alert('Error in server/cat/bib_brief.js, add_volumes(): ' + E);
+    }
+}

Modified: trunk/Open-ILS/xul/staff_client/server/cat/bib_brief.xul
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/cat/bib_brief.xul	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/server/cat/bib_brief.xul	2011-03-29 15:24:44 UTC (rev 19883)
@@ -22,7 +22,7 @@
 <?xul-overlay href="/xul/server/cat/bib_brief_overlay.xul"?>
 
 <window id="cat_bib_brief_win" 
-    onload="try { my_init(); font_helper(); persist_helper(); } catch(E) { alert(E); }"
+    onload="try { my_init(); font_helper(); persist_helper(); unhide_add_volumes_button(); } catch(E) { alert(E); }"
     xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
     <!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
@@ -39,7 +39,13 @@
     <messagecatalog id="circStrings" src="/xul/server/locale/<!--#echo var='locale'-->/circ.properties"/>
 
     <groupbox id="groupbox" flex="1">
-        <caption id="caption"><label value="&staff.cat.bib_brief.record_summary;"/>(<label value="&staff.cat.bib_brief.view_marc;" class="click_link" onclick="view_marc();"/>)</caption>
+        <caption id="caption">
+            <label value="&staff.cat.bib_brief.record_summary;"/>
+            <label id="add_volumes_left_paren" value="(" hidden="true"/>
+            <label id="add_volumes" value="&staff.cat.bib_brief.add_volumes;" class="click_link" onclick="add_volumes();" hidden="true"/>
+            <label id="add_volumes_right_paren" value=")" hidden="true"/>
+            (<label value="&staff.cat.bib_brief.view_marc;" class="click_link" onclick="view_marc();"/>)
+        </caption>
         <grid id="bib_brief_grid" />
     </groupbox>
 

Modified: trunk/Open-ILS/xul/staff_client/server/cat/copy_browser.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/cat/copy_browser.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/server/cat/copy_browser.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -288,14 +288,16 @@
 
                                     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 w = xulG.new_tab(
-                                        window.xulG.url_prefix(urls.XUL_VOLUME_COPY_CREATOR),
+                                        url,
                                         { 'tab_name' : title },
                                         {
                                             'doc_id' : obj.docid, 
                                             'ou_ids' : list, 
                                             'copy_shortcut' : copy_shortcut,
-                                            'refresh' : function() { obj.refresh_list(); }
+                                            'onrefresh' : function() { obj.refresh_list(); }
                                         }
                                     );
                                 } catch(E) {
@@ -331,7 +333,7 @@
                                 }
                             }
                         ],
-                        'cmd_replace_barcode' : [
+                        'cmd_edit_items' : [
                             ['command'],
                             function() {
                                 try {
@@ -356,12 +358,12 @@
                                     xulG.volume_item_creator( {'existing_copies':list, 'onrefresh' : function() { obj.refresh_list(); } } );
 
                                 } catch(E) {
-                                    obj.error.standard_unexpected_error_alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.replace_barcode.error'),E);
+                                    obj.error.standard_unexpected_error_alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.edit_items.error'),E);
                                     obj.refresh_list();
                                 }
                             }
                         ],
-                        'cmd_edit_items' : [
+                        'old_cmd_edit_items' : [
                             ['command'],
                             function() {
                                 try {
@@ -536,10 +538,12 @@
 
                                     var title = document.getElementById('catStrings').getString('staff.cat.copy_browser.add_volume.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 w = xulG.new_tab(
-                                        window.xulG.url_prefix(urls.XUL_VOLUME_COPY_CREATOR),
+                                        url,
                                         { 'tab_name' : title },
-                                        { 'doc_id' : obj.docid, 'ou_ids' : list, 'refresh' : function() { obj.refresh_list(); } }
+                                        { 'doc_id' : obj.docid, 'ou_ids' : list, 'onrefresh' : function() { obj.refresh_list(); } }
                                     );
 
                                 } catch(E) {
@@ -639,7 +643,6 @@
                                             }
                                             if (robj.ilsevent != 0) throw(robj);
                                         }
-                                        alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_volume.success'));
                                         obj.refresh_list();
                                     }
                                 } catch(E) {
@@ -1448,6 +1451,7 @@
             var data = {
                 'row' : {
                     'my' : {
+                        'doc_id' : obj.docid,
                         'aou' : obj.data.hash.aou[ acn_tree.owning_lib() ],
                         'acn' : acn_tree,
                         'acp' : acp_item,
@@ -1517,6 +1521,7 @@
                         'circ_lib' : { 'hidden' : false },
                         'owning_lib' : { 'hidden' : false },
                         'call_number' : { 'hidden' : false },
+                        'parts' : { 'hidden' : false },
                         'due_date' : { 'hidden' : false },
                         'acp_status' : { 'hidden' : false },
                     },
@@ -1525,8 +1530,12 @@
                             'due_date',
                             'owning_lib',
                             'circ_lib',
+                            'label_class',
+                            'prefix',
                             'call_number',
+                            'suffix',
                             'copy_number',
+                            'parts',
                             'location',
                             'barcode',
                             'loan_duration',
@@ -1668,7 +1677,6 @@
             obj.controller.view.cmd_add_items.setAttribute('disabled','true');
             obj.controller.view.cmd_add_items_to_buckets.setAttribute('disabled','true');
             obj.controller.view.cmd_edit_items.setAttribute('disabled','true');
-            obj.controller.view.cmd_replace_barcode.setAttribute('disabled','true');
             obj.controller.view.cmd_delete_items.setAttribute('disabled','true');
             obj.controller.view.cmd_print_spine_labels.setAttribute('disabled','true');
             obj.controller.view.cmd_add_volumes.setAttribute('disabled','true');
@@ -1700,7 +1708,6 @@
                 obj.controller.view.sel_mark_items_missing.setAttribute('disabled','false');
                 obj.controller.view.cmd_add_items_to_buckets.setAttribute('disabled','false');
                 obj.controller.view.cmd_edit_items.setAttribute('disabled','false');
-                obj.controller.view.cmd_replace_barcode.setAttribute('disabled','false');
                 obj.controller.view.cmd_delete_items.setAttribute('disabled','false');
                 obj.controller.view.cmd_print_spine_labels.setAttribute('disabled','false');
                 obj.controller.view.cmd_transfer_items.setAttribute('disabled','false');

Modified: trunk/Open-ILS/xul/staff_client/server/cat/copy_browser.xul
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/cat/copy_browser.xul	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/server/cat/copy_browser.xul	2011-03-29 15:24:44 UTC (rev 19883)
@@ -86,7 +86,6 @@
         <command id="cmd_add_items"/>
         <command id="cmd_add_items_to_buckets"/>
         <command id="cmd_edit_items"/>
-        <command id="cmd_replace_barcode"/>
         <command id="cmd_delete_items"/>
         <command id="cmd_transfer_items"/>
         <command id="cmd_print_spine_labels"/>
@@ -128,7 +127,6 @@
             <menuitem command="sel_mark_items_missing" label="&staff.cat.copy_browser.actions.sel_mark_items_missing.label;" accesskey="&staff.cat.copy_browser.actions.sel_mark_items_missing.accesskey;"/>
             <menuseparator/>
             <menuitem command="cmd_print_spine_labels" label="&staff.cat.copy_browser.actions.cmd_print_spine_labels.label;" accesskey="&staff.cat.copy_browser.actions.cmd_print_spine_labels.accesskey;"/>
-            <menuitem command="cmd_replace_barcode" label="&staff.cat.copy_browser.actions.cmd_replace_barcode.label;" accesskey=""/>
             <menuitem command="save_columns" label="&staff.cat.copy_browser.actions.save_columns.label;"/>
             <menuitem command="cmd_refresh_list" label="&staff.cat.copy_browser.actions.cmd_refresh_list.label;" accesskey="&staff.cat.copy_browser.actions.cmd_refresh_list.accesskey;"/>
         </popup>
@@ -142,8 +140,6 @@
             <label value="&staff.cat.copy_browser.holdings_maintenance.depth_filter_menu;" />
             <hbox id="x_depth_menu"/>
             <spacer flex="1"/>
-            <label value="&staff.cat.copy_browser.holdings_maintenance.consortial_total;"/><label id="consortial_total"/>
-            <label value="&staff.cat.copy_browser.holdings_maintenance.consortial_available;"/><label id="consortial_available"/>
         </hbox>
         <hbox>
             <checkbox id="show_acns" label="&staff.cat.copy_browser.holdings_maintenance.show_acns;" />
@@ -155,6 +151,8 @@
             <button label="Show All Libs" command="cmd_show_all_libs" accesskey=""/>
             -->
             <spacer flex="1"/>
+            <label value="&staff.cat.copy_browser.holdings_maintenance.consortial_total;"/><label id="consortial_total"/>
+            <label value="&staff.cat.copy_browser.holdings_maintenance.consortial_available;"/><label id="consortial_available"/>
             <menubar>
                 <menu label="&staff.cat.copy_browser.holdings_maintenance.actions.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.actions.accesskey;">
                     <menupopup>
@@ -182,7 +180,6 @@
                         <menuitem command="sel_mark_items_missing" label="&staff.cat.copy_browser.holdings_maintenance.sel_mark_items_missing.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.sel_mark_items_missing.accesskey;"/>
                         <menuseparator/>
                         <menuitem command="cmd_print_spine_labels" label="&staff.cat.copy_browser.holdings_maintenance.cmd_print_spine_labels.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.cmd_print_spine_labels.accesskey;"/>
-                        <menuitem command="cmd_replace_barcode" label="&staff.cat.copy_browser.holdings_maintenance.cmd_replace_barcode.label;" accesskey=""/>
                         <menuitem command="save_columns" label="&staff.cat.copy_browser.holdings_maintenance.save_columns.label;"/>
                         <menuitem command="cmd_refresh_list" label="&staff.cat.copy_browser.holdings_maintenance.cmd_refresh_list.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.cmd_refresh_list.accesskey;"/>
                     </menupopup>

Modified: trunk/Open-ILS/xul/staff_client/server/cat/copy_editor.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/cat/copy_editor.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/server/cat/copy_editor.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -2,8 +2,6 @@
 var g = {};
 g.map_acn = {};
 
-var xulG = {};
-
 function $(id) { return document.getElementById(id); }
 
 function my_init() {
@@ -24,6 +22,10 @@
         JSAN.use('OpenILS.data'); g.data = new OpenILS.data(); g.data.init({'via':'stash'});
         JSAN.use('util.network'); g.network = new util.network();
 
+        if (xulG.unified_interface) {
+            $('non_unified_buttons').hidden = true;
+        }
+
         g.docid = xul_param('docid',{'modal_xulG':true});
         g.handle_update = xul_param('handle_update',{'modal_xulG':true});
 
@@ -63,44 +65,50 @@
 
         if (xul_param('edit',{'modal_xulG':true}) == '1') { 
 
-            // Editor desired, but let's check permissions
-            g.edit = false;
+            g.edit = true;
 
-            try {
-                var check = g.network.simple_request(
-                    'PERM_MULTI_ORG_CHECK',
-                    [ 
-                        ses(), 
-                        g.data.list.au[0].id(), 
-                        util.functional.map_list(
-                            g.copies,
-                            function (o) {
-                                var lib;
-                                var cn_id = o.call_number();
-                                if (cn_id == -1) {
-                                    lib = o.circ_lib(); // base perms on circ_lib instead of owning_lib if pre-cat
-                                } else {
-                                    if (! g.map_acn[ cn_id ]) {
-                                        var req = g.network.simple_request('FM_ACN_RETRIEVE.authoritative',[ cn_id ]);
-                                        if (typeof req.ilsevent == 'undefined') {
-                                            g.map_acn[ cn_id ] = req;
+            if (g.copies.length > 0) { // When loaded in the unified interface, there may be no copies yet (from the volum/item creator) 
+
+                // Editor desired, but let's check permissions
+                g.edit = false;
+
+                try {
+                    var check = g.network.simple_request(
+                        'PERM_MULTI_ORG_CHECK',
+                        [ 
+                            ses(), 
+                            g.data.list.au[0].id(), 
+                            util.functional.map_list(
+                                g.copies,
+                                function (o) {
+                                    var lib;
+                                    var cn_id = o.call_number();
+                                    if (cn_id == -1) {
+                                        lib = o.circ_lib(); // base perms on circ_lib instead of owning_lib if pre-cat
+                                    } else {
+                                        if (! g.map_acn[ cn_id ]) {
+                                            var req = g.network.simple_request('FM_ACN_RETRIEVE.authoritative',[ cn_id ]);
+                                            if (typeof req.ilsevent == 'undefined') {
+                                                g.map_acn[ cn_id ] = req;
+                                                lib = g.map_acn[ cn_id ].owning_lib();
+                                            } else {
+                                                lib = o.circ_lib();
+                                            }
+                                        } else {
                                             lib = g.map_acn[ cn_id ].owning_lib();
-                                        } else {
-                                            lib = o.circ_lib();
                                         }
-                                    } else {
-                                        lib = g.map_acn[ cn_id ].owning_lib();
                                     }
+                                    return typeof lib == 'object' ? lib.id() : lib;
                                 }
-                                return typeof lib == 'object' ? lib.id() : lib;
-                            }
-                        ),
-                        g.copies.length == 1 ? [ 'UPDATE_COPY' ] : [ 'UPDATE_COPY', 'UPDATE_BATCH_COPY' ]
-                    ]
-                );
-                g.edit = check.length == 0;
-            } catch(E) {
-                g.error.standard_unexpected_error_alert('batch permission check',E);
+                            ),
+                            g.copies.length == 1 ? [ 'UPDATE_COPY' ] : [ 'UPDATE_COPY', 'UPDATE_BATCH_COPY' ]
+                        ]
+                    );
+                    g.edit = check.length == 0;
+                } catch(E) {
+                    g.error.standard_unexpected_error_alert('batch permission check',E);
+                }
+
             }
 
             if (g.edit) {
@@ -110,6 +118,7 @@
             } else {
                 $('top_nav').setAttribute('hidden','true');
             }
+
         } else {
             $('top_nav').setAttribute('hidden','true');
         }
@@ -162,6 +171,29 @@
         g.render();
         g.check_for_unmet_required_fields();
 
+        if (xulG.unified_interface) {
+            xulG.refresh_copy_editor = function() {
+                try {
+                    g.copies = xulG.copies;
+                    g.original_copies = js2JSON( g.copies );
+                    for (var i = 0; i < g.applied_templates.length; i++) {
+                        g._apply_template( g.applied_templates[i] );
+                    }
+                    g.summarize( g.copies );
+                    g.render();
+                    g.check_for_unmet_required_fields();
+                } catch(E) {
+                    alert('Error in copy_editor.js, xulG.refresh_copy_editor(): ' + E);
+                }
+            };
+            xulG.unlock_copy_editor = function() {
+                oils_unlock_page();
+            };
+            xulG.notify_of_templatable_field_change = function(id,v) {
+                g.changed[ 'volume_copy_creator.'+id ] = { 'type' : 'volume_copy_creator', 'field' : id, 'value' : v };
+            }
+        }
+
     } catch(E) {
         var err_msg = $("commonStrings").getFormattedString('common.exception', ['cat/copy_editor.js', E]);
         try { g.error.sdump('D_ERROR',err_msg); } catch(E) { dump(err_msg); dump(js2JSON(E)); }
@@ -191,6 +223,25 @@
             function() { g.copy_editor_prefs[ 'template_menu' ] = { 'value' : g.template_menu.value }; g.save_attributes(); },
             false
         );
+
+        if (xulG.unified_interface) {
+            if (typeof xulG.update_unified_template_list == 'function') {
+                xulG.update_unified_template_list(list);
+                // functions the unified wrapper should use to let the item attribute editor do the heavy lifting for templates
+                xulG.update_item_editor_template_selection = function(new_value) {
+                    g.template_menu.value = new_value;
+                    g.copy_editor_prefs[ 'template_menu' ] = { 'value' : g.template_menu.value };
+                    g.save_attributes();
+                }
+                xulG.item_editor_apply_template = function() { g.apply_template(); };
+                xulG.item_editor_delete_template = function() { g.delete_template(); };
+                xulG.item_editor_save_template = function() { g.save_template(); };
+                xulG.item_editor_import_templates = function() { g.import_templates(); };
+                xulG.item_editor_export_templates = function() { g.export_templates(); };
+                xulG.item_editor_reset = function() { g.reset(); };
+            }
+        }
+
     } catch(E) {
         g.error.standard_unexpected_error_alert($('catStrings').getString('staff.cat.copy_editor.retrieve_templates.error'), E);
     }
@@ -199,10 +250,26 @@
 /******************************************************************************************************/
 /* Apply Template */
 
+g.applied_templates = [];
+
 g.apply_template = function() {
     try {
         var name = g.template_menu.value;
         if (g.templates[ name ] != 'undefined') {
+            g.applied_templates.push( name );
+            g._apply_template(name);
+            g.summarize( g.copies );
+            g.render();
+            g.check_for_unmet_required_fields();
+        }
+    } catch(E) {
+        g.error.standard_unexpected_error_alert($('catStrings').getString('staff.cat.copy_editor.apply_templates.error'), E);
+    }
+}
+
+g._apply_template = function(name) {
+    try {
+        if (g.templates[ name ] != 'undefined') {
             var template = g.templates[ name ];
             for (var i in template) {
                 g.changed[ i ] = template[ i ];
@@ -216,14 +283,16 @@
                     case 'owning_lib' :
                         g.apply_owning_lib(template[i].value);
                     break;
+                    case 'volume_copy_creator' :
+                        if (xulG.unified_interface) {
+                            xulG.apply_template_to_batch(template[i].field,template[i].value);
+                        }
+                    break;
                 }
             }
-            g.summarize( g.copies );
-            g.render();
-            g.check_for_unmet_required_fields();
         }
     } catch(E) {
-        g.error.standard_unexpected_error_alert($('catStrings').getString('staff.cat.copy_editor.apply_templates.error'), E);
+        alert('Error in copy_editor.js, g._apply_template('+name+'): ' + E);
     }
 }
 
@@ -383,12 +452,16 @@
 /* Restore backup copies */
 
 g.reset = function() {
+    g.applied_templates = [];
     g.changed = {};
     g.copies = JSON2js( g.original_copies );
     g.summarize( g.copies );
     g.render();
     g.check_for_unmet_required_fields();
     oils_unlock_page();
+    if (xulG.unified_interface) {
+        xulG.reset_batch_menus();
+    }
 }
 
 /******************************************************************************************************/
@@ -459,6 +532,8 @@
 
 g.apply_owning_lib = function(ou_id) {
     g.error.sdump('D_TRACE','ou_id = ' + ou_id + '\n');
+    // but don't allow this when bundled with the volume/copy creator UI, or if we're editing pre-cats
+    if (! g.safe_to_change_owning_lib() ) { return; }
     for (var i = 0; i < g.copies.length; i++) {
         var copy = g.copies[i];
         try {
@@ -471,15 +546,15 @@
                 g.map_acn[copy.call_number()] = volume;
             }
             var old_volume = g.map_acn[copy.call_number()];
-            var acn_id = g.network.simple_request(
+            var acn_blob = g.network.simple_request(
                 'FM_ACN_FIND_OR_CREATE',
-                [ses(),old_volume.label(),old_volume.record(),ou_id]
+                [ses(),old_volume.label(),old_volume.record(),ou_id,old_volume.prefix(),old_volume.suffix(),old_volume.label_class()]
             );
-            if (typeof acn_id.ilsevent != 'undefined') {
-                g.error.standard_unexpected_error_alert($('catStrings').getFormattedString('staff.cat.copy_editor.apply_owning_lib.call_number.error', [copy.barcode()]), acn_id);
+            if (typeof acn_blob.ilsevent != 'undefined') {
+                g.error.standard_unexpected_error_alert($('catStrings').getFormattedString('staff.cat.copy_editor.apply_owning_lib.call_number.error', [copy.barcode()]), acn_blob);
                 continue;
             }
-            copy.call_number(acn_id);
+            copy.call_number(acn_blob.acn_id);
             copy.ischanged('1');
         } catch(E) {
             g.error.standard_unexpected_error_alert('apply_stat_cat',E);
@@ -490,10 +565,11 @@
 }
 
 /******************************************************************************************************/
-/* This returns true if none of the copies being edited are pre-cats */
+/* This returns false if any of the copies being edited are pre-cats, or if we're embedded in the unified volume/copy UI */
 
 g.safe_to_change_owning_lib = function() {
     try {
+        if (xulG.unified_interface) { return false; }
         var safe = true;
         for (var i = 0; i < g.copies.length; i++) {
             var cn = g.copies[i].call_number();

Modified: trunk/Open-ILS/xul/staff_client/server/cat/copy_editor.xul
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/cat/copy_editor.xul	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/server/cat/copy_editor.xul	2011-03-29 15:24:44 UTC (rev 19883)
@@ -20,7 +20,6 @@
 
 <window id="cat_copy_editor_win" 
     onload="try { my_init(); font_helper(); persist_helper(); } catch(E) { alert(E); }"
-    width="800" height="580" oils_persist="width height"
     title="&staff.cat.copy_editor.window.label;"
     xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
@@ -42,7 +41,7 @@
         <caption id="caption" label="&staff.cat.copy_editor.groupbox1.label;"/>
 
         <hbox id="top_nav">
-            <hbox style="background: grey">
+            <hbox id="template_bar" style="background: grey" flex="1">
                 <vbox><spacer flex="1"/><label value="&staff.cat.copy_editor.templates.label;" style="font-weight: bold"/><spacer flex="1"/></vbox>
                 <hbox id="template_placeholder"/>
                 <button id="apply_template" label="&staff.cat.copy_editor.templates.apply_template.label;" accesskey="&staff.cat.copy_editor.templates.apply_template.accesskey;" oncommand="g.apply_template()"/>
@@ -50,9 +49,9 @@
                 <button id="import_templates" label="&staff.cat.copy_editor.templates.import_template.label;" oncommand="g.import_templates()"/>
                 <button id="export_templates" label="&staff.cat.copy_editor.templates.export_template.label;" oncommand="g.export_templates()"/>
                 <button id="save_template" label="&staff.cat.copy_editor.templates.save_template.label;" oncommand="g.save_template()"/>
+                <spacer flex="1"/>
+                <button label="&staff.cat.copy_editor.templates.reset.label;" accesskey="&staff.cat.copy_editor.templates.reset.accesskey;" oncommand="g.reset()"/>
             </hbox>
-            <spacer flex="1"/>
-            <button label="&staff.cat.copy_editor.templates.reset.label;" accesskey="&staff.cat.copy_editor.templates.reset.accesskey;" oncommand="g.reset()"/>
         </hbox>
 
         <hbox flex="1" style="overflow: scroll">
@@ -88,8 +87,10 @@
         <hbox id="nav">
             <spacer flex="1"/>
             <button id="copy_notes" label="&staff.cat.copy_editor.copy_notes.label;" accesskey="&staff.cat.copy_editor.copy_notes.accesskey;" oncommand="g.copy_notes();"/>
-            <button id="save" label="&staff.cat.copy_editor.save.label;" hidden="true" accesskey="&staff.cat.copy_editor.save.accesskey;" oncommand="g.stash_and_close();"/>
-            <button id="cancel" label="&staff.cat.copy_editor.cancel.label;" accesskey="&staff.cat.copy_editor.cancel.accesskey;" oncommand="JSAN.use('util.widgets'); util.widgets.dispatch('close',window);"/>
+            <hbox id="non_unified_buttons">
+                <button id="save" label="&staff.cat.copy_editor.save.label;" hidden="true" accesskey="&staff.cat.copy_editor.save.accesskey;" oncommand="g.stash_and_close();"/>
+                <button id="cancel" label="&staff.cat.copy_editor.cancel.label;" accesskey="&staff.cat.copy_editor.cancel.accesskey;" oncommand="JSAN.use('util.widgets'); util.widgets.dispatch('close',window);"/>
+            </hbox>
         </hbox>
 
         <spacer/>

Modified: trunk/Open-ILS/xul/staff_client/server/cat/spine_labels.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/cat/spine_labels.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/server/cat/spine_labels.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -44,10 +44,17 @@
                         var record = g.network.simple_request('MODS_SLIM_RECORD_RETRIEVE.authoritative', [ volume.record() ]);
                         volume.record( record );
 
-                        /* Jam the prefixes and suffixes into the volume object */
-                        volume.prefix = label_prefix;
-                        volume.suffix = label_suffix;
+                        /* The volume object has native prefix and suffixes now, so affix the ones coming from copy locations */
+                        var temp_prefix = label_prefix + ' ' + (typeof volume.prefix() == 'object' ? volume.prefix().label() : volume.prefix());
+                        var temp_suffix = (typeof volume.suffix() == 'object' ? volume.suffix().label() : volume.suffix()) + ' ' + label_suffix;
 
+                        /* And assume that leading and trailing spaces can be trimmed */
+                        temp_prefix = temp_prefix.replace(/\s+$/,'').replace(/^\s+/,'');
+                        temp_suffix = temp_suffix.replace(/\s+$/,'').replace(/^\s+/,'');
+
+                        volume.prefix( temp_prefix );
+                        volume.suffix( temp_suffix );
+
                         g.volumes[ volume.id() ] = volume;
                     }
                     if (g.volumes[ copy.call_number() ].copies()) {
@@ -183,11 +190,11 @@
 
             /* Only add the prefixes and suffixes once */
             if (!override || volume.id() != override.acn) {
-                if (volume.prefix) {
-                    callnum = volume.prefix + ' ' + callnum;
+                if (volume.prefix()) {
+                    callnum = volume.prefix() + ' ' + callnum;
                 }
-                if (volume.suffix) {
-                    callnum += ' ' + volume.suffix;
+                if (volume.suffix()) {
+                    callnum += ' ' + volume.suffix();
                 }
             }
 

Modified: trunk/Open-ILS/xul/staff_client/server/cat/util.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/cat/util.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/server/cat/util.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -522,13 +522,13 @@
         JSAN.use('util.error'); error = new util.error();
         JSAN.use('util.network'); var network = new util.network();
 
-        var acn_id = network.simple_request(
+        var acn_blob = network.simple_request(
             'FM_ACN_FIND_OR_CREATE',
             [ ses(), cn_label, doc_id, ses('ws_ou') ]
         );
 
-        if (typeof acn_id.ilsevent != 'undefined') {
-            error.standard_unexpected_error_alert($("catStrings").getFormattedString('staff.cat.volume_copy_creator.stash_and_close.problem_with_volume', [cn]), acn_id);
+        if (typeof acn_blob.ilsevent != 'undefined') {
+            error.standard_unexpected_error_alert($("catStrings").getFormattedString('staff.cat.volume_copy_creator.stash_and_close.problem_with_volume', [cn]), acn_blob);
             return;
         }
 
@@ -536,7 +536,7 @@
         copy_obj.id( -1 );
         copy_obj.isnew('1');
         copy_obj.barcode( cp_barcode );
-        copy_obj.call_number( acn_id );
+        copy_obj.call_number( acn_blob.acn_id );
         copy_obj.circ_lib( ses('ws_ou') );
         /* FIXME -- use constants */
         copy_obj.deposit(0);

Modified: trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_creator.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_creator.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_creator.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -1,5 +1,16 @@
 const g_max_copies_that_can_be_added_at_a_time_per_volume = 999;
+const rel_vert_pos_volume_count = 1;
+const rel_vert_pos_call_number_classification = 2;
+const rel_vert_pos_call_number_prefix = 3;
+const rel_vert_pos_call_number = 4;
+const rel_vert_pos_call_number_suffix = 5;
+const rel_vert_pos_copy_count = 6;
+const rel_vert_pos_barcode = 7;
+const rel_vert_pos_part = 8;
+const update_timer = 1000;
 var g = {};
+g.use_defaults = true;
+g.acn_map = {}; // store retrieved acn objects here by id
 
 function my_init() {
     try {
@@ -15,12 +26,38 @@
         g.error.sdump('D_TRACE','my_init() for cat/volume_copy_creator.xul');
 
         JSAN.use('OpenILS.data'); g.data = new OpenILS.data(); g.data.init({'via':'stash'});
-        JSAN.use('util.widgets'); JSAN.use('util.functional');
+        JSAN.use('util.widgets'); JSAN.use('util.functional'); JSAN.use('util.fm_utils');
 
         JSAN.use('util.network'); g.network = new util.network();
 
         g.refresh = xul_param('onrefresh');
 
+        if (xulG.unified_interface) {
+            $('non_unified_buttons').hidden = true;
+            xulG.reset_batch_menus = function() {
+                $('batch_class_menulist').value = false;
+                util.widgets.dispatch('command',$('batch_class_menulist'));
+                $('batch_prefix_menulist').value = false;
+                util.widgets.dispatch('command',$('batch_prefix_menulist'));
+                $('batch_suffix_menulist').value = false;
+                util.widgets.dispatch('command',$('batch_suffix_menulist'));
+            }
+            xulG.apply_template_to_batch = function(id,value) {
+                if (!isNaN(Number(value))) {
+                    $(id).value = value;
+                    util.widgets.dispatch('command',$(id));
+                }
+                setTimeout(
+                    function() {
+                        // TODO:  Only apply batch to columns that haven't been adjusted manually?
+                        util.widgets.dispatch('command',$('batch_button'));
+                    },0
+                );
+            }
+        } else {
+            $('Create').hidden = true;
+        }
+
         /***********************************************************************************************************/
         /* Am I adding just copies or copies and volumes?  Or am I rebarcoding existing copies? */
 
@@ -43,11 +80,15 @@
             set_attr('EditThenCreate','accesskey','staff.cat.volume_copy_creator.edit_then_rebarcode.btn.accesskey');
             set_attr('CreateWithDefaults','label','staff.cat.volume_copy_creator.rebarcode.btn.label');
             set_attr('CreateWithDefaults','accesskey','staff.cat.volume_copy_creator.rebarcode.btn.accesskey');
+            set_attr('Create','label','staff.cat.volume_copy_creator.rebarcode.btn.label');
+            set_attr('Create','accesskey','staff.cat.volume_copy_creator.rebarcode.btn.accesskey');
         } else {
             set_attr('EditThenCreate','label','staff.cat.volume_copy_creator.edit_then_create.btn.label');
             set_attr('EditThenCreate','accesskey','staff.cat.volume_copy_creator.edit_then_create.btn.accesskey');
             set_attr('CreateWithDefaults','label','staff.cat.volume_copy_creator.create_with_defaults.btn.label');
             set_attr('CreateWithDefaults','accesskey','staff.cat.volume_copy_creator.create_with_defaults.btn.accesskey');
+            set_attr('Create','label','staff.cat.volume_copy_creator.create.btn.label');
+            set_attr('Create','accesskey','staff.cat.volume_copy_creator.create.btn.accesskey');
         }
 
         //g.error.sdump('D_ERROR','location.href = ' + location.href + '\n\ncopy_short cut = ' + g.copy_shortcut + '\n\nou_ids = ' + xul_param('ou_ids'));
@@ -56,18 +97,19 @@
 
         // Get the default callnumber classification scheme from OU settings
         dojo.require('fieldmapper.OrgUtils');
-        var label_class = g.data.hash.aous['cat.default_classification_scheme']; //fieldmapper.aou.fetchOrgSettingDefault(ses('ws_ou'), 'cat.default_classification_scheme');
+        //fieldmapper.aou.fetchOrgSettingDefault(ses('ws_ou'), 'cat.default_classification_scheme');
+        g.label_class = g.data.hash.aous['cat.default_classification_scheme'];
 
-        // Assign a default value if none was returned 
-        if (!label_class) {
-            label_class = 1;
+        // Assign a default value if none was returned
+        if (!g.label_class) {
+            g.label_class = g.data.list.acnc[0].id();
         }
 
         /***********************************************************************************************************/
-        /* If we're passed existing_copies, rig up a copy_shortcut object to leverage existing code for rendering the volume labels, etc. 
-         * Also make a lookup object for existing copies keyed on org id and callnumber label, and another keyed on copy id. */
+        /* If we're passed existing_copies, rig up a copy_shortcut object to leverage existing code for rendering the volume labels, etc.
+         * Also make a lookup object for existing copies keyed on org id and callnumber composite key, and another keyed on copy id. */
 
-        // g.org_label_existing_copy_map = { ou_id : { callnumber_label : [ copy1, copy2, ... ] }, ... }
+        // g.org_label_existing_copy_map = { ou_id : { callnumber_composite_key : [ copy1, copy2, ... ] }, ... }
         g.org_label_existing_copy_map = {};
         // g.id_copy_map = { acp_id : acp, ... }
         g.id_copy_map = {};
@@ -82,11 +124,17 @@
                 g.copy_shortcut[ call_number.owning_lib() ] = {};
                 g.org_label_existing_copy_map[ call_number.owning_lib() ] = {};
             }
-            g.copy_shortcut[ call_number.owning_lib() ][ call_number.label() ] = call_number.id();
-            if (! g.org_label_existing_copy_map[ call_number.owning_lib() ][ call_number.label() ]) {
-                g.org_label_existing_copy_map[ call_number.owning_lib() ][ call_number.label() ] = [];
+            var acnc_id = call_number.label_class() ?
+                ( typeof call_number.label_class() == 'object' ? call_number.label_class().id() : call_number.label_class() )
+                : g.label_class;
+            var acnp_id = typeof call_number.prefix() == 'object' ? call_number.prefix().id() : call_number.prefix();
+            var acns_id = typeof call_number.suffix() == 'object' ? call_number.suffix().id() : call_number.suffix();
+            var callnumber_composite_key = acnc_id + ':' + acnp_id + ':' + call_number.label() + ':' + acns_id;
+            g.copy_shortcut[ call_number.owning_lib() ][ callnumber_composite_key ] = call_number.id();
+            if (! g.org_label_existing_copy_map[ call_number.owning_lib() ][ callnumber_composite_key ]) {
+                g.org_label_existing_copy_map[ call_number.owning_lib() ][ callnumber_composite_key ] = [];
             }
-            g.org_label_existing_copy_map[ call_number.owning_lib() ][ call_number.label() ].push( copy );
+            g.org_label_existing_copy_map[ call_number.owning_lib() ][ callnumber_composite_key ].push( copy );
         }
 
         /***********************************************************************************************************/
@@ -104,18 +152,26 @@
         get_contentWindow(summary).xulG = { 'docid' : g.doc_id };
 
         /***********************************************************************************************************/
-        /* For the call number drop down */
+        /* Setup pcrud and fetch the monographic parts for this bib */
 
-        if (g.existing_copies.length > 0 || !g.copy_shortcut) {
-            g.list_callnumbers(g.doc_id, label_class);
-        }
+        dojo.require('openils.PermaCrud');
+        g.pcrud = new openils.PermaCrud({'authtoken':ses()});
+        g.parts = g.pcrud.search('bmp',{'record':g.doc_id},{'order_by': { 'bmp' : 'label_sortkey' } });
+        g.parts_hash = util.functional.convert_object_list_to_hash( g.parts );
 
         /***********************************************************************************************************/
+        /* For the batch drop downs */
+
+        g.list_classes();
+        g.list_callnumbers(g.doc_id, g.label_class);
+        g.render_batch_button();
+
+        /***********************************************************************************************************/
         /* render the orgs and volumes/input */
 
         var rows = document.getElementById('rows');
 
-        var node_id = 0;
+        g.ou_ids = [];
         for (var i = 0; i < ou_ids.length; i++) {
             try {
                 var org = g.data.hash.aou[ ou_ids[i] ];
@@ -123,14 +179,34 @@
                     var row = document.createElement('row'); rows.appendChild(row); row.setAttribute('ou_id',ou_ids[i]);
                     g.render_library_label(row,ou_ids[i]);
                     g.render_volume_count_entry( row, ou_ids[i] );
+                    g.ou_ids.push( ou_ids[i] );
                 }
             } catch(E) {
                 g.error.sdump('D_ERROR',E);
             }
         }
+        g.common_ancestor_ou_ids = util.fm_utils.find_common_aou_ancestors( g.ou_ids ).reverse();
 
+        /***********************************************************************************************************/
+        /* For the remainder batch drop downs */
+
+        g.list_prefixes();
+        g.list_suffixes();
+
+        /************/
+
         g.load_prefs();
 
+        if (g.existing_copies.length > 0) {
+            g.gather_copies_soon();
+        }
+
+        try {
+            $('main').parentNode.scrollLeft = 9999;
+        } catch(E) {
+            dump('Error in volume_copy_creator.js, my_init(), trying to auto-scroll to the far right: ' + E + '\n');
+        }
+
     } catch(E) {
         var err_msg = $("commonStrings").getFormattedString('common.exception', ['cat/volume_copy_creator.js', E]);
         try { g.error.sdump('D_ERROR',err_msg); } catch(E) { dump(err_msg); dump(js2JSON(E)); }
@@ -147,8 +223,12 @@
 g.render_volume_count_entry = function(row,ou_id) {
     var hb = document.createElement('vbox'); row.appendChild(hb);
     var tb = document.createElement('textbox'); hb.appendChild(tb);
+    if (g.use_defaults) {
+        tb.value = 1; // default to 1 volume per org
+        tb.select();
+    }
     tb.setAttribute('ou_id',ou_id); tb.setAttribute('size','3'); tb.setAttribute('cols','3');
-    tb.setAttribute('rel_vert_pos','1'); 
+    tb.setAttribute('rel_vert_pos',rel_vert_pos_volume_count);
     if ( (!g.copy_shortcut) && (!g.last_focus) ) { tb.focus(); g.last_focus = tb; }
     var node;
     function render_copy_count_entry(ev) {
@@ -161,15 +241,16 @@
                 return;
             }
             if (node) { row.removeChild(node); node = null; }
-            //ev.target.disabled = true;
             node = g.render_callnumber_copy_count_entry(row,ou_id,ev.target.value);
         }
     }
-    util.widgets.apply_vertical_tab_on_enter_handler( 
-        tb, 
-        function() { render_copy_count_entry({'target':tb}); setTimeout(function(){util.widgets.vertical_tab(tb);},0); }
+    util.widgets.apply_vertical_tab_on_enter_handler(
+        tb,
+        function() { render_copy_count_entry({'target':tb}); setTimeout(function(){util.widgets.vertical_tab(tb);},0); },
+        g.delay_gather_copies_soon
     );
     tb.addEventListener( 'change', render_copy_count_entry, false);
+    tb.addEventListener( 'change', g.gather_copies_soon, false);
     tb.addEventListener( 'focus', function(ev) { g.last_focus = ev.target; }, false );
     setTimeout(
         function() {
@@ -184,6 +265,14 @@
                     ).length;
                     render_copy_count_entry({'target':tb});
                     tb.disabled = true;
+                } else if (tb.value) {
+                    // since we're now supplying a default
+                    render_copy_count_entry({'target':tb});
+                    setTimeout(
+                        function() {
+                            util.widgets.vertical_tab(tb);
+                        }, 0
+                    );
                 }
             } catch(E) {
                 alert(E);
@@ -199,14 +288,35 @@
     var rows = grid.lastChild;
     var r = document.createElement('row'); rows.appendChild( r );
     var x = document.createElement('label'); r.appendChild(x);
-    x.setAttribute('value', $("catStrings").getString('staff.cat.volume_copy_creator.render_callnumber_copy_count_entry.call_nums')); x.setAttribute('style','font-weight: bold');
+        x.setAttribute('value', $("catStrings").getString('staff.cat.volume_copy_creator.render_callnumber_copy_count_entry.classification'));
+        x.setAttribute('style','font-weight: bold');
     x = document.createElement('label'); r.appendChild(x);
-    x.setAttribute('value',$("catStrings").getString('staff.cat.volume_copy_creator.render_callnumber_copy_count_entry.num_of_copies')); x.setAttribute('style','font-weight: bold');
-    x.setAttribute('size','3'); x.setAttribute('cols','3');
+        x.setAttribute('value', $("catStrings").getString('staff.cat.volume_copy_creator.render_callnumber_copy_count_entry.prefix'));
+        x.setAttribute('style','font-weight: bold');
+    x = document.createElement('label'); r.appendChild(x);
+        x.setAttribute('value', $("catStrings").getString('staff.cat.volume_copy_creator.render_callnumber_copy_count_entry.call_nums'));
+        x.setAttribute('style','font-weight: bold');
+    x = document.createElement('label'); r.appendChild(x);
+        x.setAttribute('value', $("catStrings").getString('staff.cat.volume_copy_creator.render_callnumber_copy_count_entry.suffix'));
+        x.setAttribute('style','font-weight: bold');
+    x = document.createElement('label'); r.appendChild(x);
+        x.setAttribute('value',$("catStrings").getString('staff.cat.volume_copy_creator.render_callnumber_copy_count_entry.num_of_copies'));
+        x.setAttribute('style','font-weight: bold');
+    x = document.createElement('label'); r.appendChild(x);
+        x.setAttribute('value',$("catStrings").getString('staff.cat.volume_copy_creator.render_callnumber_copy_count_entry.barcodes_and_parts'));
+        x.setAttribute('style','font-weight: bold');
 
-    function handle_change(call_number_column_textbox,number_of_copies_column_textbox,barcode_column_box) {
-        if (call_number_column_textbox.value == '') return;
-        if (isNaN( Number( number_of_copies_column_textbox.value ) )) return;
+    function handle_change_precipitating_barcode_rendering(
+        callnumber_composite_key,
+        number_of_copies_column_textbox,
+        barcode_column_box
+    ) {
+        dump('handle_change_precipitating_barcode_rendering\n');
+
+        if (isNaN( Number( number_of_copies_column_textbox.value ) )) {
+            dump('1:handle_change_precipitating_barcode_rendering early return\n');
+            return;
+        }
         if ( Number( number_of_copies_column_textbox.value ) > g_max_copies_that_can_be_added_at_a_time_per_volume ) {
             g.error.yns_alert($("catStrings").getFormattedString('staff.cat.volume_copy_creator.render_volume_count_entry.message', [g_max_copies_that_can_be_added_at_a_time_per_volume]),
                 $("catStrings").getString('staff.cat.volume_copy_creator.render_volume_count_entry.title'),
@@ -219,84 +329,215 @@
         }
         g.render_barcode_entry(
             barcode_column_box,
-            call_number_column_textbox.value,
+            callnumber_composite_key,
             Number(number_of_copies_column_textbox.value),
             ou_id
         );
 
-        document.getElementById("EditThenCreate").disabled = false;
-        document.getElementById("CreateWithDefaults").disabled = false;
+        if (! xulG.unified_interface) {
+            document.getElementById("EditThenCreate").disabled = false;
+            document.getElementById("CreateWithDefaults").disabled = false;
+        } else {
+            document.getElementById("Create").disabled = false;
+        }
     }
 
-    function handle_change_call_number_column_textbox(ev) {
-        var _call_number_column_textbox = ev.target;    
+    function handle_change_to_callnumber_data(ev) {
+        dump('handle_change_to_callnumber_data\n');
+        var _call_number_column_textbox = ev.target;
         var _call_number_column_box = _call_number_column_textbox.parentNode;
-        var _number_of_copies_column_box = _call_number_column_box.nextSibling;
+
+        var _classification_column_box = _call_number_column_box.previousSibling.previousSibling; /* two over to the left */
+        var _classification_column_menulist = _classification_column_box.firstChild;
+
+        var _prefix_column_box = _call_number_column_box.previousSibling; /* one over to the left */
+        var _prefix_column_menulist = _prefix_column_box.firstChild;
+
+        var _suffix_column_box = _call_number_column_box.nextSibling; /* one over to the right */
+        var _suffix_column_menulist = _suffix_column_box.firstChild;
+
+        var _number_of_copies_column_box = _call_number_column_box.nextSibling.nextSibling; /* two over to the right */
         var _number_of_copies_column_textbox = _number_of_copies_column_box.firstChild;
+
         var _barcode_column_box = _number_of_copies_column_box.nextSibling;
-        handle_change(_call_number_column_textbox,_number_of_copies_column_textbox,_barcode_column_box);
+
+        var acn_label = _call_number_column_textbox.value;
+        var acnc_id = _classification_column_menulist.value;
+        var acnp_id = _prefix_column_menulist.value;
+        var acns_id = _suffix_column_menulist.value;
+        var callnumber_composite_key = acnc_id + ':' + acnp_id + ':' + acn_label + ':' + acns_id;
+        dump('\tcomposite_key = ' + callnumber_composite_key + '\n');
+
+        _call_number_column_textbox.setAttribute('callkey',callnumber_composite_key);
+        _call_number_column_textbox.setAttribute('acnc_id',acnc_id);
+        _call_number_column_textbox.setAttribute('acnp_id',acnp_id);
+        _call_number_column_textbox.setAttribute('acns_id',acns_id);
+
+        handle_change_precipitating_barcode_rendering(
+            callnumber_composite_key,
+            _number_of_copies_column_textbox,
+            _barcode_column_box
+        );
     }
 
     function handle_change_number_of_copies_column_textbox(ev) {
-        var _number_of_copies_column_textbox = ev.target;    
+        dump('handle_change_number_of_copies_column_textbox\n');
+        var _number_of_copies_column_textbox = ev.target;
         var _number_of_copies_column_box = _number_of_copies_column_textbox.parentNode;
-        var _call_number_column_box = _number_of_copies_column_box.previousSibling;
+        var _call_number_column_box = _number_of_copies_column_box.previousSibling.previousSibling; /* two over */
         var _call_number_column_textbox = _call_number_column_box.firstChild;
-        var _barcode_column_box = _number_of_copies_column_box.nextSibling;
-        handle_change(_call_number_column_textbox,_number_of_copies_column_textbox,_barcode_column_box);
+        handle_change_to_callnumber_data({'target':_call_number_column_textbox}); // let this guy do the work
     }
 
     for (var i = 0; i < count; i++) {
         var r = document.createElement('row'); rows.appendChild(r);
-        var call_number_column_box = document.createElement('vbox'); r.appendChild(call_number_column_box);
-        var number_of_copies_column_box = document.createElement('vbox'); r.appendChild(number_of_copies_column_box);
-        var barcode_column_box = document.createElement('vbox'); r.appendChild(barcode_column_box);
-        var call_number_column_textbox = document.createElement('textbox'); call_number_column_box.appendChild(call_number_column_textbox);
-        call_number_column_textbox.setAttribute('rel_vert_pos','2');
-        call_number_column_textbox.setAttribute('ou_id',ou_id);
-        util.widgets.apply_vertical_tab_on_enter_handler( 
-            call_number_column_textbox, 
-            function() { handle_change_call_number_column_textbox({'target':call_number_column_textbox}); setTimeout(function(){util.widgets.vertical_tab(call_number_column_textbox);},0); }
-        );
-        var number_of_copies_column_textbox = document.createElement('textbox'); number_of_copies_column_box.appendChild(number_of_copies_column_textbox);
-        number_of_copies_column_textbox.setAttribute('size','3'); number_of_copies_column_textbox.setAttribute('cols','3');
-        number_of_copies_column_textbox.setAttribute('rel_vert_pos','3');
-        number_of_copies_column_textbox.setAttribute('ou_id',ou_id);
-        util.widgets.apply_vertical_tab_on_enter_handler( 
-            number_of_copies_column_textbox, 
-            function() { handle_change_number_of_copies_column_textbox({'target':number_of_copies_column_textbox}); setTimeout(function(){util.widgets.vertical_tab(number_of_copies_column_textbox);},0); }
-        );
 
-        call_number_column_textbox.addEventListener( 'change', handle_change_call_number_column_textbox, false);
-        call_number_column_textbox.addEventListener( 'focus', function(ev) { g.last_focus = ev.target; }, false );
-        number_of_copies_column_textbox.addEventListener( 'change', handle_change_number_of_copies_column_textbox, false);
-        number_of_copies_column_textbox.addEventListener( 'focus', function(ev) { g.last_focus = ev.target; }, false );
-        if ( !g.last_focus ) { number_of_copies_column_textbox.focus(); g.last_focus = number_of_copies_column_textbox; }
+            /**** CLASSIFICATION COLUMN ****/
+            var classification_column_box = document.createElement('vbox');
+            r.appendChild(classification_column_box);
 
+            /**** PREFIX COLUMN ****/
+            var prefix_column_box = document.createElement('vbox');
+            r.appendChild(prefix_column_box);
+
+            /**** CALLNUMBER COLUMN ****/
+            var call_number_column_box = document.createElement('vbox');
+            r.appendChild(call_number_column_box);
+                var call_number_column_textbox = document.createElement('textbox');
+                call_number_column_box.appendChild(call_number_column_textbox);
+                    if (g.use_defaults && $('marc_cn').firstChild) {
+                        // default to first real value from batch callnumber menu
+                        var menupopup = $('marc_cn').firstChild.firstChild;
+                        if (menupopup.childNodes.length > 1) {
+                            call_number_column_textbox.value = menupopup.childNodes[1].getAttribute('label');
+                            call_number_column_textbox.select();
+                        }
+                    }
+                    call_number_column_textbox.setAttribute('rel_vert_pos',rel_vert_pos_call_number);
+                    call_number_column_textbox.setAttribute('ou_id',ou_id);
+                    util.widgets.apply_vertical_tab_on_enter_handler(
+                        call_number_column_textbox,
+                        function() {
+                            handle_change_to_callnumber_data({'target':call_number_column_textbox});
+                            setTimeout(
+                                function(){
+                                    util.widgets.vertical_tab(call_number_column_textbox);
+                                },0
+                            );
+                        },
+                        g.delay_gather_copies_soon
+                    );
+                    call_number_column_textbox.addEventListener( 'change', handle_change_to_callnumber_data, false);
+                    call_number_column_textbox.addEventListener( 'change', g.gather_copies_soon, false);
+                    call_number_column_textbox.addEventListener( 'focus', function(ev) { g.last_focus = ev.target; }, false );
+
+                    /**** CLASSIFICATION COLUMN revisited ****/
+                    var classification_column_menulist = g.render_class_menu(call_number_column_textbox);
+                    classification_column_menulist.addEventListener(
+                        'command',
+                        function() {
+                            handle_change_to_callnumber_data({'target':call_number_column_textbox});
+                        }
+                        ,false
+                    );
+                    classification_column_box.appendChild(classification_column_menulist);
+
+                    /**** PREFIX COLUMN revisited ****/
+                    var prefix_column_menulist = g.render_prefix_menu(call_number_column_textbox);
+                    prefix_column_menulist.addEventListener(
+                        'command',
+                        function() {
+                            handle_change_to_callnumber_data({'target':call_number_column_textbox});
+                        }
+                        ,false
+                    );
+
+                    prefix_column_box.appendChild(prefix_column_menulist);
+
+            /**** SUFFIX COLUMN ****/
+            var suffix_column_box = document.createElement('vbox');
+            r.appendChild(suffix_column_box);
+                var suffix_column_menulist = g.render_suffix_menu(call_number_column_textbox);
+                suffix_column_menulist.addEventListener(
+                    'command',
+                    function() {
+                        handle_change_to_callnumber_data({'target':call_number_column_textbox});
+                    }
+                    ,false
+                );
+                suffix_column_box.appendChild(suffix_column_menulist);
+
+            /**** NUMBER OF COPIES COLUMN ****/
+            var number_of_copies_column_box = document.createElement('vbox');
+            r.appendChild(number_of_copies_column_box);
+                var number_of_copies_column_textbox = document.createElement('textbox');
+                number_of_copies_column_box.appendChild(number_of_copies_column_textbox);
+                    if (g.use_defaults) {
+                        // default to one copy per call number
+                        number_of_copies_column_textbox.value = 1;
+                        number_of_copies_column_textbox.select();
+                    }
+                    number_of_copies_column_textbox.setAttribute('size','3'); number_of_copies_column_textbox.setAttribute('cols','3');
+                    number_of_copies_column_textbox.setAttribute('rel_vert_pos',rel_vert_pos_copy_count);
+                    number_of_copies_column_textbox.setAttribute('ou_id',ou_id);
+                    util.widgets.apply_vertical_tab_on_enter_handler(
+                        number_of_copies_column_textbox,
+                        function() {
+                            handle_change_number_of_copies_column_textbox({'target':number_of_copies_column_textbox});
+                            setTimeout(
+                                function(){
+                                    util.widgets.vertical_tab(number_of_copies_column_textbox);
+                                },0
+                            );
+                        },
+                        g.delay_gather_copies_soon
+                    );
+                    number_of_copies_column_textbox.addEventListener( 'change', handle_change_number_of_copies_column_textbox, false);
+                    number_of_copies_column_textbox.addEventListener( 'change', g.gather_copies_soon, false);
+                    number_of_copies_column_textbox.addEventListener( 'focus', function(ev) { g.last_focus = ev.target; }, false );
+                    if ( !g.last_focus ) { number_of_copies_column_textbox.focus(); g.last_focus = number_of_copies_column_textbox; }
+
+            /**** BARCODE COLUMN ****/
+            var barcode_column_box = document.createElement('vbox');
+            r.appendChild(barcode_column_box);
+
         setTimeout(
             function(idx,call_number_column_textbox,number_of_copies_column_textbox){
                 return function() {
                     try {
                         JSAN.use('util.functional');
                         if (g.copy_shortcut) {
-                            var label = util.functional.map_object_to_list(
+                            var callnumber_composite_key = util.functional.map_object_to_list(
                                 g.copy_shortcut[ou_id],
                                 function(o,i) {
                                     return i;
                                 }
                             )[idx];
                             if (g.org_label_existing_copy_map[ou_id]) {
-                                var num_of_copies = g.org_label_existing_copy_map[ou_id][label].length;
+                                var num_of_copies = g.org_label_existing_copy_map[ou_id][callnumber_composite_key].length;
                                 if (num_of_copies>0) {
                                     number_of_copies_column_textbox.value = num_of_copies;
                                     number_of_copies_column_textbox.disabled = true;
                                 }
                             }
-                            call_number_column_textbox.value = label; 
-                            handle_change_call_number_column_textbox({'target':call_number_column_textbox});
-                            if (g.existing_copies.length < 1) {
-                                call_number_column_textbox.disabled = true;
+                            var acn_label = callnumber_composite_key.split(/:/).slice(2,-1).join(':');
+                            var acnc_id = callnumber_composite_key.split(/:/)[0];
+                            var acnp_id = callnumber_composite_key.split(/:/)[1];
+                            var acns_id = callnumber_composite_key.split(/:/).slice(-1)[0];
+                            call_number_column_textbox.value = acn_label;
+                            classification_column_menulist.value = acnc_id;
+                            prefix_column_menulist.value = acnp_id;
+                            suffix_column_menulist.value = acns_id;
+                            handle_change_to_callnumber_data({'target':call_number_column_textbox});
+                        } else {
+
+                            // if we're providing defaults, keep on rendering
+                            if (call_number_column_textbox.value) {
+                                util.widgets.dispatch('change',call_number_column_textbox);
                             }
+                            if (number_of_copies_column_textbox.value) {
+                                util.widgets.dispatch('change',number_of_copies_column_textbox);
+                            }
                         }
                     } catch(E) {
                         alert(E);
@@ -309,37 +550,151 @@
     return grid;
 }
 
-g.render_barcode_entry = function(node,callnumber,count,ou_id) {
+g.render_part_menu = function(barcode_tb) {
+    var hbox = document.createElement('hbox');
+    var menulist = document.createElement('menulist');
+        menulist.setAttribute('editable','true');
+        hbox.appendChild(menulist);
+    var button = document.createElement('button');
+        button.setAttribute('label',$('catStrings').getString('staff.cat.volume_copy_creator.create_part.btn.label'));
+        button.hidden = true;
+        hbox.appendChild(button);
+
+    var menupopup = document.createElement('menupopup');
+        menulist.appendChild(menupopup);
+        g.render_part_menuitems(menupopup);
+
+    button.addEventListener(
+        'command',
+        function(ev) {
+            var new_part = new bmp();
+                new_part.isnew(1);
+                new_part.label(menulist.value);
+                new_part.record(g.doc_id);
+            g.pcrud.create(new_part, {
+                "oncomplete": function (r, objs) {
+                    var db_part = objs[0];
+                    if (!db_part) { return; }
+                    g.parts.push( db_part );
+                    g.parts_hash[ db_part.id() ] = db_part;
+                    g.render_part_menuitems(menupopup);
+                    if (menulist.selectedItem) {
+                        barcode_tb.setAttribute('bmp_id',menulist.selectedItem.value);
+                        button.hidden = true;
+                    }
+                    g.gather_copies_soon();
+                }
+            });
+        },
+        false
+    );
+
+    menulist.addEventListener(
+        'change',
+        function(ev) {
+            if (! ev.target.selectedItem) {
+                button.hidden = false;
+            }
+        },
+        false
+    );
+    menulist.addEventListener('change',g.gather_copies_soon,false);
+    menulist.addEventListener(
+        'command',
+        function(ev) {
+            barcode_tb.setAttribute('bmp_id',menulist.selectedItem.value);
+            button.hidden = true;
+        },
+        false
+    );
+    menulist.addEventListener('command',g.gather_copies_soon,false);
+
+    return hbox;
+}
+
+g.render_part_menuitems = function(menupopup) {
+    util.widgets.remove_children(menupopup);
+    var menuitem = document.createElement('menuitem');
+    menuitem.setAttribute('label','');
+    menuitem.setAttribute('value','');
+    menupopup.appendChild(menuitem);
+    for (var i = 0; i < g.parts.length; i++) {
+        var menuitem = document.createElement('menuitem');
+        menuitem.setAttribute('label',g.parts[i].label());
+        menuitem.setAttribute('value',g.parts[i].id());
+        menupopup.appendChild(menuitem);
+    }
+
+}
+
+g.render_barcode_entry = function(node,callnumber_composite_key,count,ou_id) {
     try {
+        dump('g.render_barcode_entry(node,'+callnumber_composite_key+','+count+','+ou_id+'\n');
         function ready_to_create(ev) {
-            document.getElementById("EditThenCreate").disabled = false;
-            document.getElementById("CreateWithDefaults").disabled = false;
+            if (! xulG.unified_interface) {
+                document.getElementById("EditThenCreate").disabled = false;
+                document.getElementById("CreateWithDefaults").disabled = false;
+            } else {
+                document.getElementById("Create").disabled = false;
+            }
         }
 
-        JSAN.use('util.barcode'); 
+        JSAN.use('util.barcode');
 
         for (var i = 0; i < count; i++) {
-            var tb; var set_handlers = false;
+            var tb_part_box;
+            var tb;
+            var part_menu;
+            var set_handlers = false;
             if (typeof node.childNodes[i] == 'undefined') {
-                tb = document.createElement('textbox'); node.appendChild(tb);
+                tb_part_box = document.createElement('hbox');
+                node.appendChild(tb_part_box);
+                tb = document.createElement('textbox');
+                tb_part_box.appendChild(tb);
+                part_menu = g.render_part_menu(tb);
+                tb_part_box.appendChild(part_menu);
                 set_handlers = true;
             } else {
-                tb = node.childNodes[i];
+                tb_part_box = node.childNodes[i];
+                tb = tb_part_box.firstChild;
+                part_menu = tb_part_box.lastChild;
             }
             tb.setAttribute('ou_id',ou_id);
-            tb.setAttribute('callnumber',callnumber);
-            tb.setAttribute('rel_vert_pos','4');
+            tb.setAttribute('callkey',callnumber_composite_key);
+            tb.setAttribute('rel_vert_pos',rel_vert_pos_barcode);
+            part_menu.firstChild.setAttribute('rel_vert_pos',rel_vert_pos_part);
             if (!tb.value && g.org_label_existing_copy_map[ ou_id ]) {
-                tb.value = g.org_label_existing_copy_map[ ou_id ][ callnumber ][i].barcode();
-                tb.setAttribute('acp_id', g.org_label_existing_copy_map[ ou_id ][ callnumber ][i].id());
+                tb.value = g.org_label_existing_copy_map[ ou_id ][ callnumber_composite_key ][i].barcode();
+                tb.setAttribute('acp_id', g.org_label_existing_copy_map[ ou_id ][ callnumber_composite_key ][i].id());
+                var temp_parts = g.org_label_existing_copy_map[ ou_id ][ callnumber_composite_key ][i].parts();
+                temp_parts = util.functional.filter_list(
+                    temp_parts,
+                    function(p) {
+                        return p.record() == g.doc_id; // filter out foreign parts
+                    }
+                );
+                if (temp_parts.length > 0) {
+                    tb.setAttribute('bmp_id',temp_parts[0].id());
+                    part_menu.firstChild.value = g.parts_hash[ temp_parts[0].id() ].label();
+                }
                 tb.select();
                 if (! g.first_focus) { g.first_focus = tb; }
             }
+            if (g.use_defaults && ! g.first_focus) {
+                g.first_focus = tb;
+                tb.focus();
+            }
             if (set_handlers) {
-                util.widgets.apply_vertical_tab_on_enter_handler( 
-                    tb, 
-                    function() { ready_to_create({'target':tb}); setTimeout(function(){util.widgets.vertical_tab(tb);},0); }
+                util.widgets.apply_vertical_tab_on_enter_handler(
+                    tb,
+                    function() { ready_to_create({'target':tb}); setTimeout(function(){util.widgets.vertical_tab(tb);},0); },
+                    g.delay_gather_copies_soon
                 );
+                util.widgets.apply_vertical_tab_on_enter_handler(
+                    part_menu.firstChild,
+                    function() { setTimeout(function(){util.widgets.vertical_tab(part_menu.firstChild);},0); },
+                    g.delay_gather_copies_soon
+                );
                 tb.addEventListener('change', function(ev) {
                     var barcode = String( ev.target.value ).replace(/\s/g,'');
                     if (barcode != ev.target.value) ev.target.value = barcode;
@@ -351,12 +706,14 @@
                         setTimeout( function() { ev.target.select(); ev.target.focus(); }, 0);
                     }
                 }, false);
+                tb.addEventListener('change', g.gather_copies_soon, false);
                 tb.addEventListener( 'focus', function(ev) { g.last_focus = ev.target; }, false );
             }
         }
-        
-        setTimeout( function() { if (g.first_focus) { g.first_focus.focus(); } }, 0 ); 
 
+        g.gather_copies_soon();
+        setTimeout( function() { if (g.first_focus) { g.first_focus.focus(); } }, 0 );
+
     } catch(E) {
         g.error.sdump('D_ERROR','g.render_barcode_entry: ' + E);
     }
@@ -364,7 +721,7 @@
 
 g.generate_barcodes = function() {
     try {
-        var nodes = document.getElementsByAttribute('rel_vert_pos','4');
+        var nodes = document.getElementsByAttribute('rel_vert_pos',rel_vert_pos_barcode);
         if (nodes.length < 1) { return; }
         var first_barcode = nodes[0].value;
 
@@ -387,50 +744,167 @@
         for (var i = 0; i < barcodes.length; i++) {
             nodes[i+1].value = barcodes[i];
             nodes[i+1].select();
+            util.widgets.dispatch('change',nodes[i+1]);
         }
 
+        setTimeout(
+            function() {
+                g.gather_copies_soon();
+            },0
+        );
+
     } catch(E) {
         g.error.sdump('D_ERROR','g.generate_barcodes: ' + E);
     }
 }
 
-g.new_node_id = -1;
+g.delay_gather_copies_soon = function() {
+    if (xulG.unified_interface) {
+        dump('g.delay_gather_copies_soon()\n');
+        g.gather_copies_soon();
+    }
+}
 
-g.stash_and_close = function(param) {
+g.gather_copies_soon = function() {
+    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);
+        }
+        // This function is expensive when it comes to keeping the UI responsive, so let's give it a delay
+        // that quick entry of consecutive fields can override
+        g.update_copy_editor_timeoutID = setTimeout(
+            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);
+                }
+            }, update_timer
+        );
+    } catch(E) {
+        alert('Error in volume_copy_creator.js, g.gather_copies_soon(): ' + E);
+    }
+}
 
+g.new_acp_id = -1;
+g.new_acn_id = -1;
+
+g.gather_copies = function() {
+    dump('g.gather_copies()\n');
     try {
-
         var nl = document.getElementsByTagName('textbox');
 
-        var volumes_hash = {};
+        g.volumes_scaffold = {};
+        /*
+            g.volumes_scaffold = {
+                '#ou_id' : {
+                    '#class_id:#prefix_id:#callnumber label:#suffix_id' : {
+                        'callnumber_data' : {
+                            'acn_id' : '#callnumber id',
+                            'acn_label' : '#callnumber label',
+                            'acnc_id' : '#classification_id',
+                            'acnp_id' : '#prefix_id',
+                            'acns_id' : '#suffix_id'
+                        },
+                        'barcode_data' :
+                            [
+                                {
+                                    'barcode' : '#barcode',
+                                    'acp_id' : '#copy_id',
+                                    'bmp_id' : '#part_id'
+                                }, ...
+                            ]
+                    }
+                }, ...
+            }
+        */
 
         var barcodes = [];
-        
+        var v_count = 0;
         for (var i = 0; i < nl.length; i++) {
-            if ( nl[i].getAttribute('rel_vert_pos') == 4 ) barcodes.push( nl[i] );
-            if ( nl[i].getAttribute('rel_vert_pos') == 2 )  {
+            if ( nl[i].getAttribute('rel_vert_pos') == rel_vert_pos_barcode ) barcodes.push( nl[i] );
+            if ( nl[i].getAttribute('rel_vert_pos') == rel_vert_pos_call_number )  {
+                v_count++;
                 var ou_id = nl[i].getAttribute('ou_id');
+                var acn_id = nl[i].getAttribute('acn_id');
+                if (!acn_id) {
+                    acn_id = g.new_acn_id--;
+                    nl[i].setAttribute('acn_id',acn_id);
+                }
+                var acnc_id = nl[i].getAttribute('acnc_id') || g.label_class;
+                var acnp_id = nl[i].getAttribute('acnp_id') || -1;
+                var acns_id = nl[i].getAttribute('acns_id') || -1;
                 var callnumber = nl[i].value;
-                if (typeof volumes_hash[ou_id] == 'undefined') { volumes_hash[ou_id] = {} }
-                if (typeof volumes_hash[ou_id][callnumber] == 'undefined') { volumes_hash[ou_id][callnumber] = [] }
+                if (typeof g.volumes_scaffold[ou_id] == 'undefined') {
+                    g.volumes_scaffold[ou_id] = {}
+                }
+                var composite_key = acnc_id + ':' + acnp_id + ':' + callnumber + ':' + acns_id;
+                if (typeof g.volumes_scaffold[ou_id][composite_key] == 'undefined') {
+                    g.volumes_scaffold[ou_id][composite_key] = {
+                        //'node' : nl[i],
+                        'callnumber_data' : {
+                            'acn_id' : acn_id,
+                            'acn_label' : callnumber,
+                            'acnc_id' : acnc_id,
+                            'acnp_id' : acnp_id,
+                            'acns_id' : acns_id
+                        },
+                        'barcode_data' : []
+                    }
+                    dump('fleshing volumes scaffold with ou_id = ' + ou_id + ' composite_key = ' + composite_key + ' acn_id = ' + acn_id + '\n');
+                }
             }
         };
-    
+        dump('volume_copy_creator: processed ' + nl.length + ' textbox nodes, consisting of ' + barcodes.length + ' barcodes and ' + v_count + 'volumes\n');
+        dump('volume scaffold = ' + js2JSON(g.volumes_scaffold) + '\n');
+
         for (var i = 0; i < barcodes.length; i++) {
-            var acp_id = barcodes[i].getAttribute('acp_id') || g.new_node_id--;
+            var acp_id = barcodes[i].getAttribute('acp_id') || g.new_acp_id--;
             var ou_id = barcodes[i].getAttribute('ou_id');
-            var callnumber = barcodes[i].getAttribute('callnumber');
+            var callnumber_composite_key = barcodes[i].getAttribute('callkey');
             var barcode = barcodes[i].value;
+            var bmp_id = barcodes[i].getAttribute('bmp_id');
 
-            if (typeof volumes_hash[ou_id] == 'undefined') { volumes_hash[ou_id] = {} }
-            if (typeof volumes_hash[ou_id][callnumber] == 'undefined') { volumes_hash[ou_id][callnumber] = [] }
+            dump('placing ' + barcode + ' for ou = ' + ou_id + ' into composite_key bin ' + callnumber_composite_key + '\n');
 
-            if (barcode != '') volumes_hash[ou_id][callnumber].push( { 'barcode' : barcode, 'acp_id' : acp_id } );
+            if (typeof g.volumes_scaffold[ou_id] == 'undefined') {
+                dump('1: I want to remove this soon, so alert me if it is getting used, ou_id = ' + ou_id + '\n');
+                g.volumes_scaffold[ou_id] = {}
+            }
+            if (typeof g.volumes_scaffold[ou_id][callnumber_composite_key] == 'undefined') {
+                dump('2: when does this happen, and why? ou_id = ' + ou_id + ' callnumber_composite_key = ' + callnumber_composite_key + '\n');
+                // one way this can happen, race condition between this function and editing a widget
+                g.volumes_scaffold[ou_id][callnumber_composite_key] = {
+                    'callnumber_data' : {
+                        // not ideal, but hey...
+                        'acn_label' : callnumber_composite_key.split(/:/).slice(2,-1).join(':'),
+                        'acnc_id' : callnumber_composite_key.split(/:/)[0],
+                        'acnp_id' : callnumber_composite_key.split(/:/)[1],
+                        'acns_id' : callnumber_composite_key.split(/:/).slice(-1)[0]
+                    },
+                    'barcode_data' : []
+                }
+            }
+
+            if (barcode != '') {
+                g.volumes_scaffold[ou_id][callnumber_composite_key].barcode_data.push(
+                    {
+                        'barcode' : barcode,
+                        'acp_id' : acp_id,
+                        'bmp_id' : bmp_id
+                    }
+                );
+            }
         }
 
         var volumes = [];
         var copies = [];
-        var volume_labels = {};
+        var volume_data = {};
 
         function new_copy(acp_id,ou_id,acn_id,barcode) {
             var copy = new acp();
@@ -454,43 +928,163 @@
             return copy;
         }
 
-        for (var ou_id in volumes_hash) {
-            for (var cn_label in volumes_hash[ou_id]) {
+        for (var ou_id in g.volumes_scaffold) {
+            for (var composite_key in g.volumes_scaffold[ou_id]) {
+                for (var i = 0; i < g.volumes_scaffold[ou_id][composite_key].barcode_data.length; i++) {
+                    var barcode = g.volumes_scaffold[ou_id][composite_key].barcode_data[i].barcode;
+                    var acp_id = g.volumes_scaffold[ou_id][composite_key].barcode_data[i].acp_id;
+                    var bmp_id = g.volumes_scaffold[ou_id][composite_key].barcode_data[i].bmp_id;
+                    var acn_id = g.volumes_scaffold[ou_id][composite_key].callnumber_data.acn_id;
+                    dump('gather_copies(): barcode = ' + barcode + ' acp_id = ' + acp_id + ' bmp_id = ' + bmp_id + ' acn_id = ' + acn_id + ' composite_key = ' + composite_key + '\n');
+                    var copy = g.id_copy_map[ acp_id ];
+                    if (!copy) {
+                        copy = new_copy(acp_id,ou_id,acn_id,barcode);
+                        g.id_copy_map[ acp_id ] = copy;
+                    } else {
+                        copy.ischanged( get_db_true() );
+                    }
+                    copy.barcode( barcode );
+                    copy.call_number( acn_id );
+                    var temp_parts = util.functional.filter_list(
+                        copy.parts() || [],
+                        function(p) {
+                            return (p.record() != g.doc_id); // filter out parts for this bib
+                        }
+                    );
+                    if (bmp_id) {
+                        temp_parts.push( g.parts_hash[ bmp_id ] );
+                    }
+                    copy.parts( temp_parts );
+                    copies.push( copy );
+                }
+            }
+        }
 
-                var acn_id = g.network.simple_request(
-                    'FM_ACN_FIND_OR_CREATE',
-                    [ ses(), cn_label, g.doc_id, ou_id ]
-                );
+        xulG.copies = copies;
+        return copies;
 
-                if (typeof acn_id.ilsevent != 'undefined') {
-                    g.error.standard_unexpected_error_alert($("catStrings").getFormattedString('staff.cat.volume_copy_creator.stash_and_close.problem_with_volume', [cn]), acn_id);
-                    continue;
-                }
+    } catch(E) {
+        alert('Error in volume_copy_creator.js, g.gather_copies():' + E);
+    }
+}
 
-                volume_labels[ acn_id ] = { 'label' : cn_label, 'owning_lib' : ou_id };
+g.vivicate_update_volumes = function() {
+    try {
+        var volumes = [];
+        for (var ou_id in g.volumes_scaffold) {
+            for (var composite_key in g.volumes_scaffold[ou_id]) {
 
-                for (var i = 0; i < volumes_hash[ou_id][cn_label].length; i++) {
-                    var barcode = volumes_hash[ou_id][cn_label][i].barcode;
-                    var acp_id = volumes_hash[ou_id][cn_label][i].acp_id;
-                    var copy;
-                    if (acp_id < 0) {
-                        copy = new_copy(acp_id,ou_id,acn_id,barcode);
-                    } else {
-                        copy = g.id_copy_map[ acp_id ];
-                        copy.barcode( barcode );
-                        copy.call_number( acn_id );
-                        copy.ischanged('1');
+                var callnumber_data = g.volumes_scaffold[ou_id][composite_key].callnumber_data;
+                var acn_id = callnumber_data.acn_id;
+                var acnp_id = callnumber_data.acnp_id;
+                var acns_id = callnumber_data.acns_id;
+                var acnc_id = callnumber_data.acnc_id;
+
+                if (acn_id < 0) {
+
+                    var acn_blob = g.network.simple_request(
+                        'FM_ACN_FIND_OR_CREATE',
+                        [ ses(), callnumber_data.acn_label, g.doc_id, ou_id, acnp_id, acns_id, acnc_id ]
+                    );
+                    dump('FM_ACN_FIND_OR_CREATE: label = ' + callnumber_data.acn_label
+                        + ' doc = ' + g.doc_id + ' ou = ' + ou_id + ' acnp = ' + acnp_id + ' acns = ' + acns_id + ' acnc = ' + acnc_id + '\n');
+
+                    if (typeof acn_blob.ilsevent != 'undefined') {
+                        alert('Error in g.vivicate_update_volumes, acn_id = ' + acn_id + ' acn_blob = ' + js2JSON(acn_blob));
+                        continue;
                     }
-                    copies.push( copy );
+
+                    acn_id = acn_blob.acn_id;
+
+                    if (typeof g.acn_map[ acn_id ] == 'undefined') {
+                        var temp_acn = g.network.simple_request(
+                            'FM_ACN_RETRIEVE.authoritative',
+                            [ acn_id ]
+                        );
+                        if (typeof temp_acn.ilsevent != 'undefined') {
+                            alert('Error in g.vivicate_update_volumes, acn_id = ' + acn_id + ' temp_acn = ' + js2JSON(temp_acn));
+                            continue;
+                        }
+                        g.acn_map[ acn_id ] = temp_acn;
+                        if (callnumber_data.acn_id < 0) {
+                            g.acn_map[ callnumber_data.acn_id ] = temp_acn;
+                        }
+                    }
+
                 }
+/*
+                var my_acn = g.acn_map[ acn_id ];
+
+                var node = g.volumes_scaffold[ou_id][composite_key].node;
+                var class_menulist = node.parentNode.previousSibling.previousSibling.firstChild;
+                var prefix_menulist = node.parentNode.previousSibling.firstChild;
+                var suffix_menulist = node.parentNode.nextSibling.firstChild;
+
+                if ( String(class_menulist.value) != String(my_acn.label_class()) {
+                    my_acn.label_class( class_menulist.value );
+                    my_acn.ischanged( get_db_true() );
+                }
+                if ( String(prefix_menulist.value) != String(my_acn.prefix()) {
+                    my_acn.prefix( prefix_menulist.value );
+                    my_acn.ischanged( get_db_true() );
+                }
+                if ( String(suffix_menulist.value) != String(my_acn.suffix()) {
+                    my_acn.suffix( suffix_menulist.value );
+                    my_acn.ischanged( get_db_true() );
+                }
+
+                if (get_bool( my_acn.ischanged() )) {
+                    volumes.push( my_acn );
+                }
+*/
             }
         }
+        if (volumes.length > 0) {
+            if (typeof xul_param('update_volume') == 'function') {
+                xul_param('update_volume')(volumes);
+            } else {
+                 var r = g.network.simple_request(
+                    'FM_ACN_TREE_UPDATE',
+                    [ ses(),volumes, false, { 'auto_merge_vols' : false } ]
+                );
+                if (typeof r.ilsevent != 'undefined') {
+                    alert('error with volume update: ' + js2JSON(r));
+                }
+            }
+        }
+    } catch(E) {
+        alert('Error in volume_copy_creator.js, vivicate_volumes(): ' + E);
+    }
+}
 
+g.stash_and_close = function(param) {
+
+    try {
+
+        var copies;
+        if (xulG.unified_interface) {
+            copies = xulG.copies;
+        } else {
+            copies = g.gather_copies();
+            copies = blob.copies;
+        }
+
         var dont_close = false;
-        JSAN.use('util.window'); var win = new util.window();
+
+        g.vivicate_update_volumes();
+        for (var i = 0; i < copies.length; i++) {
+            var acn_id = copies[i].call_number();
+            if (typeof g.acn_map[acn_id] != 'undefined') {
+                // handle vivicated-callnumbers
+                copies[i].call_number( g.acn_map[acn_id].id() );
+            } else {
+                alert('error in stash and close, acn_id = ' + acn_id);
+            }
+        }
+
         if (copies.length > 0) {
-            JSAN.use('cat.util');
             if (param == 'edit') {
+                JSAN.use('cat.util');
                 copies = cat.util.spawn_copy_editor( { 'edit' : true, 'docid' : g.doc_id, 'copies' : copies, 'caller_handles_update' : true });
             }
             if (typeof xul_param('update_copy') == 'function') {
@@ -501,7 +1095,7 @@
                     [ ses(),copies, true ]
                 );
                 if (typeof r.ilsevent != 'undefined') {
-                    g.error.standard_unexpected_error_alert('copy update',r);
+                    alert('error with copy update:' + js2JSON(r));
                 }
             }
             try {
@@ -513,22 +1107,26 @@
                         urls.XUL_SPINE_LABEL,
                         { 'tab_name' : $("catStrings").getString('staff.cat.util.spine_editor.tab_name') },
                         {
-                            'barcodes' : util.functional.map_list( copies, function(o){return o.barcode();}) 
+                            'barcodes' : util.functional.map_list( copies, function(o){return o.barcode();})
                         }
                     );
                 }
             } catch(E) {
-                g.error.standard_unexpected_error_alert($(catStrings).getString('staff.cat.volume_copy_creator.stash_and_close.tree_err2'),E);
+                alert('2: Error in volume_copy_creator.js with g.stash_and_close(): ' + E);
             }
         }
 
         try { if (typeof window.refresh == 'function') { window.refresh(); } } catch(E) { dump(E+'\n'); }
         try { if (typeof g.refresh == 'function') { g.refresh(); } } catch(E) { dump(E+'\n'); }
 
+        if (typeof xulG.unlock_copy_editor == 'function') {
+            xulG.unlock_copy_editor();
+        }
+
         if (! dont_close) { xulG.close_tab(); }
 
     } catch(E) {
-        g.error.standard_unexpected_error_alert($(catStrings).getString('staff.cat.volume_copy_creator.stash_and_close.tree_err3'),E);
+        alert('3: Error in volume_copy_creator.js with g.stash_and_close(): ' + E);
     }
 }
 
@@ -559,8 +1157,7 @@
 
         }
     } catch(E) {
-        g.error.standard_unexpected_error_alert($(catStrings).getString('staff.cat.volume_copy_creator.load_prefs.err_retrieving_prefs'),E);
-        
+        alert('Error in volume_copy_creator.js with g.load_prefs(): ' + E);
     }
 }
 
@@ -576,10 +1173,143 @@
         );
         file.close();
     } catch(E) {
-        g.error.standard_unexpected_error_alert($(catStrings).getString('staff.cat.volume_copy_creator.save_prefs.err_storing_prefs'),E);
+        alert('Error in volume_copy_creator.js with g.save_prefs(): ' + E);
     }
 }
 
+g.render_class_menu = function(call_number_tb) {
+    var ml = util.widgets.make_menulist(
+        util.functional.map_list(
+            g.data.list.acnc,
+            function(o) {
+                return [ o.name(), o.id() ];
+            }
+        )
+    );
+    ml.setAttribute('rel_vert_pos',rel_vert_pos_call_number_classification);
+    ml.addEventListener(
+        'command',
+        function() {
+            call_number_tb.setAttribute('acnc_id',ml.value);
+        },
+        false
+    );
+    return ml;
+}
+
+g.render_prefix_menu = function(call_number_tb) {
+    var ou_id = call_number_tb.getAttribute('ou_id');
+    var org = g.data.hash.aou[ ou_id ];
+    var menulist = document.createElement('menulist');
+        var menupopup = document.createElement('menupopup');
+        menulist.appendChild(menupopup);
+        var org_list = []; // order from top of consortium to owning lib
+        while(org) {
+            org_list.unshift(org.id());
+            org = org.parent_ou();
+            if (org && typeof org != 'object') {
+                org = g.data.hash.aou[ org ];
+            }
+        }
+        for (var i = 0; i < org_list.length; i++) {
+            g.render_prefix_menu_items(menupopup,org_list[i]);
+        }
+
+    menulist.setAttribute('rel_vert_pos',rel_vert_pos_call_number_prefix);
+    menulist.addEventListener(
+        'command',
+        function() {
+            call_number_tb.setAttribute('acnp_id',menulist.value);
+        },
+        false
+    );
+    return menulist;
+}
+
+g.render_prefix_menu_items = function(menupopup,ou_id) {
+    if (typeof g.data.list['acnp_for_lib_'+ou_id] == 'undefined') {
+        g.data.list['acnp_for_lib_'+ou_id] = g.network.simple_request(
+            'FM_ACNP_RETRIEVE_VIA_PCRUD',
+            [ ses(), {"owning_lib":{"=":ou_id}}, {"order_by":{"acnp":"label_sortkey"}} ]
+        );
+        g.data.stash('list');
+    }
+    for (var i = 0; i < g.data.list['acnp_for_lib_'+ou_id].length; i++) {
+        var my_acnp = g.data.list['acnp_for_lib_'+ou_id][i];
+        var menuitem = document.createElement('menuitem');
+        menupopup.appendChild(menuitem);
+            menuitem.setAttribute(
+                'label',
+                my_acnp.id() == -1 ? '' :
+                $('catStrings').getFormattedString(
+                    'staff.cat.volume_copy_creator.call_number_prefix.menuitem_label',
+                    [
+                        my_acnp.label(),
+                        g.data.hash.aou[ ou_id ].shortname()
+                    ]
+                )
+            );
+            menuitem.setAttribute('value',my_acnp.id());
+    }
+}
+
+g.render_suffix_menu = function(call_number_tb) {
+    var ou_id = call_number_tb.getAttribute('ou_id');
+    var org = g.data.hash.aou[ ou_id ];
+    var menulist = document.createElement('menulist');
+        var menupopup = document.createElement('menupopup');
+        menulist.appendChild(menupopup);
+        var org_list = []; // order from top of consortium to owning lib
+        while(org) {
+            org_list.unshift(org.id());
+            org = org.parent_ou();
+            if (org && typeof org != 'object') {
+                org = g.data.hash.aou[ org ];
+            }
+        }
+        for (var i = 0; i < org_list.length; i++) {
+            g.render_suffix_menu_items(menupopup,org_list[i]);
+        }
+
+    menulist.setAttribute('rel_vert_pos',rel_vert_pos_call_number_suffix);
+    menulist.addEventListener(
+        'command',
+        function() {
+            call_number_tb.setAttribute('acns_id',menulist.value);
+        },
+        false
+    );
+    return menulist;
+}
+
+g.render_suffix_menu_items = function(menupopup,ou_id) {
+    if (typeof g.data.list['acns_for_lib_'+ou_id] == 'undefined') {
+        g.data.list['acns_for_lib_'+ou_id] = g.network.simple_request(
+            'FM_ACNS_RETRIEVE_VIA_PCRUD',
+            [ ses(), {"owning_lib":{"=":ou_id}}, {"order_by":{"acns":"label_sortkey"}} ]
+        );
+        g.data.stash('list');
+    }
+    for (var i = 0; i < g.data.list['acns_for_lib_'+ou_id].length; i++) {
+        var my_acns = g.data.list['acns_for_lib_'+ou_id][i];
+        var menuitem = document.createElement('menuitem');
+        menupopup.appendChild(menuitem);
+            menuitem.setAttribute(
+                'label',
+                my_acns.id() == -1 ? '' :
+                $('catStrings').getFormattedString(
+                    'staff.cat.volume_copy_creator.call_number_suffix.menuitem_label',
+                    [
+                        my_acns.label(),
+                        g.data.hash.aou[ ou_id ].shortname()
+                    ]
+                )
+            );
+            menuitem.setAttribute('value',my_acns.id());
+    }
+}
+
+
 g.list_callnumbers = function(doc_id, label_class) {
     var cn_blob;
     try {
@@ -589,18 +1319,113 @@
     }
     var hbox = document.getElementById('marc_cn');
     var ml = util.widgets.make_menulist(
-        util.functional.map_list(
-            cn_blob,
-            function(o) {
-                for (var i in o) {
-                    return [ o[i], i ];
+        [
+            [ '', '' ]
+        ].concat(
+            util.functional.map_list(
+                cn_blob,
+                function(o) {
+                    for (var i in o) {
+                        return [ o[i], i ];
+                    }
                 }
-            }
+            )
         )
     ); hbox.appendChild(ml);
     ml.setAttribute('editable','true');
     ml.setAttribute('width', '200');
+    ml.setAttribute('id', 'marc_cn_menulist');
+}
+
+g.list_classes = function() {
+    var hbox = $('batch_class');
+    var ml = util.widgets.make_menulist(
+        [
+            [ '<No Change>', false ]
+        ].concat(
+            util.functional.map_list(
+                g.data.list.acnc,
+                function(o) {
+                    return [ o.name(), o.id() ];
+                }
+            )
+        )
+    ); hbox.appendChild(ml);
+    ml.setAttribute('id','batch_class_menulist');
+    ml.addEventListener(
+        'command',
+        function() {
+            if (!isNaN(Number(ml.value))) {
+                addCSSClass(hbox,'copy_editor_field_changed');
+                if (xulG.unified_interface) {
+                    xulG.notify_of_templatable_field_change('batch_class_menulist',ml.value);
+                }
+            } else {
+                removeCSSClass(hbox,'copy_editor_field_changed');
+            }
+        },
+        false
+    );
+}
+
+g.list_prefixes = function() {
+    var hbox = $('batch_prefix');
+    var ml = util.widgets.make_menulist(
+        [
+            [ '<No Change>', false ]
+        ]
+    ); hbox.appendChild(ml);
+    for (var i = 0; i < g.common_ancestor_ou_ids.length; i++) {
+        g.render_prefix_menu_items(ml.firstChild,g.common_ancestor_ou_ids[i]);
+    }
+    ml.setAttribute('id','batch_prefix_menulist');
+    ml.addEventListener(
+        'command',
+        function() {
+            if (!isNaN(Number(ml.value))) {
+                addCSSClass(hbox,'copy_editor_field_changed');
+                if (xulG.unified_interface) {
+                    xulG.notify_of_templatable_field_change('batch_prefix_menulist',ml.value);
+                }
+            } else {
+                removeCSSClass(hbox,'copy_editor_field_changed');
+            }
+        },
+        false
+    );
+}
+
+g.list_suffixes = function() {
+    var hbox = $('batch_suffix');
+    var ml = util.widgets.make_menulist(
+        [
+            [ '<No Change>', false ]
+        ]
+    ); hbox.appendChild(ml);
+    for (var i = 0; i < g.common_ancestor_ou_ids.length; i++) {
+        g.render_suffix_menu_items(ml.firstChild,g.common_ancestor_ou_ids[i]);
+    }
+    ml.setAttribute('id','batch_suffix_menulist');
+    ml.addEventListener(
+        'command',
+        function() {
+            if (!isNaN(Number(ml.value))) {
+                addCSSClass(hbox,'copy_editor_field_changed');
+                if (xulG.unified_interface) {
+                    xulG.notify_of_templatable_field_change('batch_suffix_menulist',ml.value);
+                }
+            } else {
+                removeCSSClass(hbox,'copy_editor_field_changed');
+            }
+        },
+        false
+    );
+}
+
+g.render_batch_button = function() {
+    var hbox = $('batch_button_box');
     var btn = document.createElement('button');
+    btn.setAttribute('id','batch_button');
     btn.setAttribute('label',$('catStrings').getString('staff.cat.volume_copy_creator.my_init.btn.label'));
     btn.setAttribute('accesskey',$('catStrings').getString('staff.cat.volume_copy_creator.my_init.btn.accesskey'));
     btn.setAttribute('image','/xul/server/skin/media/images/down_arrow.gif');
@@ -610,15 +1435,49 @@
         function() {
             var nl = document.getElementsByTagName('textbox');
             for (var i = 0; i < nl.length; i++) {
-                if (nl[i].getAttribute('rel_vert_pos')==2 
-                    && !nl[i].disabled) 
-                {
-                    nl[i].value = ml.value;
-                    util.widgets.dispatch('change',nl[i]);
+                /* label */
+                if (nl[i].getAttribute('rel_vert_pos')==rel_vert_pos_call_number && !nl[i].disabled) {
+                    var label =  $('marc_cn').firstChild.value;
+                    if (label != '') {
+                        nl[i].value = label;
+                        util.widgets.dispatch('change',nl[i]);
+                    }
                 }
             }
+            nl = document.getElementsByTagName('menulist');
+            for (var i = 0; i < nl.length; i++) {
+                /* classification */
+                if (nl[i].getAttribute('rel_vert_pos')==rel_vert_pos_call_number_classification && !nl[i].disabled) {
+                    var value =  $('batch_class_menulist').value;
+                    if (!isNaN( Number(value) )) {
+                        nl[i].value = value;
+                        util.widgets.dispatch('command',nl[i]);
+                    }
+                }
+                /* prefix */
+                if (nl[i].getAttribute('rel_vert_pos')==rel_vert_pos_call_number_prefix && !nl[i].disabled) {
+                    var value =  $('batch_prefix_menulist').value;
+                    if (!isNaN( Number(value) )) {
+                        nl[i].value = value;
+                        util.widgets.dispatch('command',nl[i]);
+                    }
+                }
+                /* suffix */
+                if (nl[i].getAttribute('rel_vert_pos')==rel_vert_pos_call_number_suffix && !nl[i].disabled) {
+                    var value =  $('batch_suffix_menulist').value;
+                    if (!isNaN( Number(value) )) {
+                        nl[i].value = value;
+                        util.widgets.dispatch('command',nl[i]);
+                    }
+                }
+            }
+            setTimeout(
+                function() {
+                    g.gather_copies_soon();
+                },0
+            );
             if (g.last_focus) setTimeout( function() { g.last_focus.focus(); }, 0 );
-        }, 
+        },
         false
     );
 }

Modified: trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_creator.xul
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_creator.xul	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_creator.xul	2011-03-29 15:24:44 UTC (rev 19883)
@@ -6,6 +6,7 @@
 <!-- STYLESHEETS -->
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
 <?xml-stylesheet href="/xul/server/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/cat.css" type="text/css"?>
 
 <!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
 <!-- LOCALIZATION -->
@@ -19,7 +20,6 @@
 
 <window id="cat_volume_copy_creator_win" 
     onload="try { my_init(); font_helper(); persist_helper(); } catch(E) { alert(E); }"
-    width="800" height="580" oils_persist="height width sizemode"
     title="&staff.cat.volume_copy_creator.title;"
     xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
@@ -36,31 +36,69 @@
     <messagecatalog id="catStrings" src="/xul/server/locale/<!--#echo var='locale'-->/cat.properties" />
     <messagecatalog id="circStrings" src="/xul/server/locale/<!--#echo var='locale'-->/circ.properties" />
 
-    <vbox id="summary_box"/>
-    <hbox>
-        <hbox id="marc_cn"/>
-        <spacer flex="1" />
-        <button id="generate_barcodes" label="&staff.cat.volume_copy_creator.generate_barcodes.label;" oncommand="g.generate_barcodes();" accesskey="&staff.cat.volume_copy_creator.generate_barcodes.accesskey;"/>
-        <checkbox id="check_barcodes" label="&staff.cat.volume_copy_creator.check_barcodes.label;" oncommand="g.save_prefs();" accesskey="&staff.cat.volume_copy_creator.check_barcodes.accesskey;"/>
-        <checkbox id="print_labels" label="&staff.cat.volume_copy_creator.print_labels.label;"  oncommand="g.save_prefs();" accesskey="&staff.cat.volume_copy_creator.print_labels.accesskey;"/>
-    </hbox>
-    <groupbox flex="1" class="my_overflow">
-        <caption id="caption" label="&staff.cat.volume_copy_creator.label;"/>
-        <grid flex="1">
-            <columns> <column flex="0"/> <column flex="0"/> <column flex="1"/> </columns>
-            <rows id="rows">
-                <row>
-                    <label value="&staff.cat.volume_copy_creator.library_label.value;" style="font-weight: bold"/>
-                    <label value="&staff.cat.volume_copy_creator.num_of_volumes_label.value;" style="font-weight: bold"/>
-                </row>
-            </rows>
-        </grid>
-    </groupbox>
-    <hbox style="border-bottom: solid black thin">
-        <spacer flex="1" />
-        <button id="CreateWithDefaults" disabled="true" oncommand="g.stash_and_close('noedit');"/>
-        <button id="EditThenCreate" disabled="true" oncommand="g.stash_and_close('edit');"/>
-    </hbox>
+<vbox flex="1" class="my_overflow">
+    <vbox id="summary_box" oils_persist="height"/>
+    <splitter
+        collapse="before"
+        resize_before="flex"
+        resize_after="flex"
+        oils_persist="state hidden"
+        oils_persist_peers="summary_box main">
+        <grippy/>
+    </splitter>
+    <vbox id="main" oils_persist="height" flex="1">
+        <hbox flex="0">
+            <hbox id="batch_bar">
+                <label value="&staff.cat.volume_copy_creator.batch_bar;"/>
+                <label value="&staff.cat.volume_copy_creator.batch_bar.call_number.classification;"/>
+                <hbox id="batch_class"/>
+                <label value="&staff.cat.volume_copy_creator.batch_bar.call_number.prefix;"/>
+                <hbox id="batch_prefix"/>
+                <label value="&staff.cat.volume_copy_creator.batch_bar.call_number.label.label;"
+                    accesskey="&staff.cat.volume_copy_creator.batch_bar.call_number.label.accesskey;" control="marc_cn_menulist"/>
+                <hbox id="marc_cn"/>
+                <label value="&staff.cat.volume_copy_creator.batch_bar.call_number.suffix;"/>
+                <hbox id="batch_suffix"/>
+                <hbox id="batch_button_box"/>
+            </hbox>
+            <spacer flex="1" />
+        </hbox>
+        <groupbox flex="1" class="my_overflow">
+            <caption id="caption" label="&staff.cat.volume_copy_creator.label;"/>
+            <grid flex="1">
+                <columns> <column flex="0"/> <column flex="0"/> <column flex="1"/> </columns>
+                <rows id="rows">
+                    <row>
+                        <label value="&staff.cat.volume_copy_creator.library_label.value;" style="font-weight: bold"/>
+                        <label value="&staff.cat.volume_copy_creator.num_of_volumes_label.value;" style="font-weight: bold"/>
+                    </row>
+                </rows>
+            </grid>
+        </groupbox>
+        <hbox style="border-bottom: solid black thin" flex="0">
+            <hbox id="misc_control_bar">
+                <button id="generate_barcodes"
+                    label="&staff.cat.volume_copy_creator.generate_barcodes.label;"
+                    oncommand="g.generate_barcodes();"
+                    accesskey="&staff.cat.volume_copy_creator.generate_barcodes.accesskey;"/>
+                <checkbox id="check_barcodes"
+                    label="&staff.cat.volume_copy_creator.check_barcodes.label;"
+                    oncommand="g.save_prefs();"
+                    accesskey="&staff.cat.volume_copy_creator.check_barcodes.accesskey;"/>
+                <checkbox id="print_labels"
+                    label="&staff.cat.volume_copy_creator.print_labels.label;"
+                    oncommand="g.save_prefs();"
+                    accesskey="&staff.cat.volume_copy_creator.print_labels.accesskey;"/>
+            </hbox>
+            <spacer flex="1"/>
+            <hbox id="non_unified_buttons">
+                <button id="CreateWithDefaults" disabled="true" oncommand="g.stash_and_close('noedit');"/>
+                <button id="EditThenCreate" disabled="true" oncommand="g.stash_and_close('edit');"/>
+            </hbox>
+            <button id="Create" disabled="true" oncommand="g.stash_and_close('unified_interface');"/>
+        </hbox>
+    </vbox>
+</vbox>
 
 </window>
 

Added: trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_editor.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_editor.js	                        (rev 0)
+++ trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_editor.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -0,0 +1,111 @@
+var error;
+var g = {};
+
+function my_init() {
+    try {
+        netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+        if (typeof JSAN == 'undefined') { throw( "The JSAN library object is missing."); }
+        JSAN.errorLevel = "die"; // none, warn, or die
+        JSAN.addRepository('/xul/server/');
+        JSAN.use('util.error'); error = new util.error();
+        error.sdump('D_TRACE','my_init() for main_test.xul');
+
+        /*if (typeof window.xulG == 'object' && typeof window.xulG.set_tab_name == 'function') {
+            try { window.xulG.set_tab_name('Test'); } catch(E) { alert(E); }
+        }*/
+
+        // Both interfaces look for this
+        xulG.unified_interface = true;
+
+        // Item Attribute Editor looks for these
+        xulG.not_modal = true;
+        xulG.edit = true;
+
+        // Spawn the volume/copy creator
+        JSAN.use('util.browser');
+        var volume_pane = new util.browser();
+        volume_pane.init(
+            {
+                'url' : urls.XUL_VOLUME_COPY_CREATOR_ORIGINAL,
+                'push_xulG' : true,
+                'alt_print' : false,
+                'browser_id' : 'volume_pane',
+                'passthru_content_params' : xulG
+            }
+        );
+
+        setup_templates();
+
+        // Spawn the item attribute editor
+        var item_pane = new util.browser();
+        item_pane.init(
+            {
+                'url' : urls.XUL_COPY_EDITOR,
+                'push_xulG' : true,
+                'alt_print' : false,
+                'browser_id' : 'item_pane',
+                'passthru_content_params' : xulG,
+                'on_url_load' : g.clone_template_bar // from setup_templates()
+            }
+        );
+
+    } catch(E) {
+        alert('Error in volume_copy_editor.js, my_init(): ' + E);
+    }
+}
+
+function setup_templates() {
+    try {
+        JSAN.use('util.widgets'); JSAN.use('util.functional');
+
+        // Once the item attribute editor is loaded, clone and import its template menu to this window with this callback
+        g.clone_template_bar = function() {
+            var item_editor_template_bar = get_contentWindow( $('item_pane') ).document.getElementById('template_bar');
+            $('template_bar_holder').appendChild(
+                document.importNode(
+                    item_editor_template_bar,
+                    true // children
+                )
+            );
+            item_editor_template_bar.hidden = true;
+            g.apply_template = function() {
+                xulG.update_item_editor_template_selection( $('template_menu').value );
+                xulG.item_editor_apply_template();
+            };
+            g.delete_template = function() {
+                xulG.update_item_editor_template_selection( $('template_menu').value );
+                xulG.item_editor_delete_template();
+            };
+            g.save_template = function() { xulG.item_editor_save_template(); };
+            g.import_templates = function() { xulG.item_editor_import_templates(); };
+            g.export_templates = function() { xulG.item_editor_apply_templates(); };
+            g.reset = function() { xulG.item_editor_reset(); };
+
+            // just do this once; not sure if on_url_load could fire multiple times
+            g.clone_template_bar = function() {};
+        }
+
+        // callback for populating the list of templates
+        xulG.update_unified_template_list = function(list) {
+            try {
+                util.widgets.remove_children('template_placeholder');
+                g.template_menu = util.widgets.make_menulist( list );
+                g.template_menu.setAttribute('id','template_menu');
+                $('template_placeholder').appendChild(g.template_menu);
+                g.template_menu.addEventListener(
+                    'command',
+                    function() {
+                        xulG.update_item_editor_template_selection( g.template_menu.value );
+                    },
+                    false
+                );
+            } catch(E) {
+                alert('Error in volume_copy_editor.js, xulG.update_unified_template_list(): ' + E);
+            }
+        };
+
+    } catch(E) {
+        alert('Error in volume_copy_editor.js, setup_templates(): ' + E);
+    }
+}
+

Copied: trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_editor.xul (from rev 19880, trunk/Open-ILS/xul/staff_client/server/cat/bib_brief.xul)
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_editor.xul	                        (rev 0)
+++ trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_editor.xul	2011-03-29 15:24:44 UTC (rev 19883)
@@ -0,0 +1,54 @@
+<?xml version="1.0"?>
+<!-- Application: Evergreen Staff Client -->
+<!-- Screen: Unified Call Number / Item Editor/Creator -->
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- STYLESHEETS -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/global.css" type="text/css"?>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- LOCALIZATION -->
+<!DOCTYPE window PUBLIC "" ""[
+    <!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- OVERLAYS -->
+<?xul-overlay href="/xul/server/OpenILS/util_overlay.xul"?>
+
+<window id="volume_item_win"
+    onload="try { my_init(); font_helper(); persist_helper(); } catch(E) { alert(E); }"
+    xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+    <!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+    <!-- BEHAVIOR -->
+    <script type="text/javascript">
+        var myPackageDir = 'open_ils_staff_client'; var IAMXUL = true;
+    </script>
+    <scripts id="openils_util_scripts"/>
+
+    <script type="text/javascript" src="/xul/server/main/JSAN.js"/>
+    <script type="text/javascript" src="volume_copy_editor.js"/>
+
+    <vbox flex="1">
+        <vbox id="top_pane" flex="1" oils_persist="height">
+            <hbox id="template_bar_holder"/>
+            <browser id="volume_pane" flex="1" />
+        </vbox>
+        <splitter
+            collapse="after"
+            resizeafter="flex"
+            resizebefore="flex"
+            oils_persist="state hidden"
+            oils_persist_peers="top_pane bottom_pane">
+            <grippy/>
+        </splitter>
+        <vbox id="bottom_pane" flex="1" oils_persist="height">
+            <browser id="item_pane" flex="1" />
+            <hbox id="bottom_bar"/>
+        </vbox>
+    </vbox>
+
+</window>
+

Copied: trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_editor_horiz.xul (from rev 19880, trunk/Open-ILS/xul/staff_client/server/cat/bib_brief.xul)
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_editor_horiz.xul	                        (rev 0)
+++ trunk/Open-ILS/xul/staff_client/server/cat/volume_copy_editor_horiz.xul	2011-03-29 15:24:44 UTC (rev 19883)
@@ -0,0 +1,54 @@
+<?xml version="1.0"?>
+<!-- Application: Evergreen Staff Client -->
+<!-- Screen: Unified Call Number / Item Editor/Creator -->
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- STYLESHEETS -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/global.css" type="text/css"?>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- LOCALIZATION -->
+<!DOCTYPE window PUBLIC "" ""[
+    <!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- OVERLAYS -->
+<?xul-overlay href="/xul/server/OpenILS/util_overlay.xul"?>
+
+<window id="volume_item_win"
+    onload="try { my_init(); font_helper(); persist_helper(); } catch(E) { alert(E); }"
+    xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+    <!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+    <!-- BEHAVIOR -->
+    <script type="text/javascript">
+        var myPackageDir = 'open_ils_staff_client'; var IAMXUL = true;
+    </script>
+    <scripts id="openils_util_scripts"/>
+
+    <script type="text/javascript" src="/xul/server/main/JSAN.js"/>
+    <script type="text/javascript" src="volume_copy_editor.js"/>
+
+    <hbox flex="1">
+        <vbox id="top_pane" flex="1" oils_persist="width">
+            <hbox id="template_bar_holder"/>
+            <browser id="volume_pane" flex="1" />
+        </vbox>
+        <splitter
+            collapse="after"
+            resizeafter="flex"
+            resizebefore="flex"
+            oils_persist="state hidden"
+            oils_persist_peers="top_pane bottom_pane">
+            <grippy/>
+        </splitter>
+        <vbox id="bottom_pane" flex="1" oils_persist="width">
+            <browser id="item_pane" flex="1" />
+            <hbox id="bottom_bar"/>
+        </vbox>
+    </hbox>
+
+</window>
+

Added: trunk/Open-ILS/xul/staff_client/server/cat/volume_editor.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/cat/volume_editor.js	                        (rev 0)
+++ trunk/Open-ILS/xul/staff_client/server/cat/volume_editor.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -0,0 +1,217 @@
+var xulG = {};
+
+function my_init() {
+    try {
+        netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+        if (typeof JSAN == 'undefined') { throw( $("commonStrings").getString('common.jsan.missing') ); }
+        JSAN.errorLevel = "die"; // none, warn, or die
+        JSAN.addRepository('/xul/server/');
+        JSAN.use('util.error'); g.error = new util.error();
+        g.error.sdump('D_TRACE','my_init() for cat/volume_editor.xul');
+
+        JSAN.use('OpenILS.data'); g.data = new OpenILS.data(); g.data.init({'via':'stash'});
+        JSAN.use('util.network'); g.network = new util.network();
+
+        JSAN.use('util.functional');
+
+        g.volumes = xul_param('volumes',{'stash_name':'volumes_temp','clear_xpcom':true,'modal_xulG':true}); //JSON2js( g.data.volumes_temp );
+        //g.data.volumes_temp = ''; g.data.stash('volumes_temp');
+
+        var rows = document.getElementById('rows');
+
+        var first_tb;
+
+        for (var i = 0; i < g.volumes.length; i++) {
+            var row = document.createElement('row'); rows.appendChild(row);
+            var lib_label = document.createElement('label'); row.appendChild(lib_label);
+            var class_ml = g.render_class_menu(i); row.appendChild(class_ml);
+            var prefix_ml = g.render_prefix_menu(i); row.appendChild(prefix_ml);
+            var label_tb = document.createElement('textbox'); row.appendChild(label_tb);
+            var suffix_ml = g.render_suffix_menu(i); row.appendChild(suffix_ml);
+            if (!first_tb) { first_tb = label_tb; }
+
+            var lib_id = g.volumes[i].owning_lib();
+            var last_lib_seen;
+
+            if (last_lib_seen != lib_id ) {
+                lib_label.setAttribute('value',g.data.hash.aou[ lib_id ].shortname() );
+                last_lib_seen = lib_id;
+            }
+
+            label_tb.setAttribute('value',g.volumes[i].label());
+            label_tb.setAttribute('onchange','try { var v = g.volumes['+i+']; v.ischanged("1"); v.label( this.value ); } catch(E) { alert(E); }');
+        }
+
+        first_tb.select(); first_tb.focus();
+
+    } catch(E) {
+        var err_msg = $("commonStrings").getFormattedString('common.exception', ['cat/volume_editor.xul', E]);
+        try { g.error.sdump('D_ERROR',err_msg); } catch(E) { dump(err_msg); dump(js2JSON(E)); }
+        alert(err_msg);
+    }
+}
+
+g.stash_and_close = function() {
+    try {
+        //g.data.volumes_temp = js2JSON( g.volumes );
+        //g.error.sdump('D_CAT','in modal window, g.data.volumes_temp = \n' + g.data.volumes_temp + '\n');
+        //g.data.stash('volumes_temp');
+        xulG.volumes = g.volumes;
+        xulG.update_these_volumes = 1;
+        xulG.auto_merge = document.getElementById('auto_merge').checked;
+        update_modal_xulG(xulG);
+        window.close();
+    } catch(E) {
+        alert('FIXME: volume editor -> ' + E);
+    }
+}
+
+g.render_class_menu = function(vol_idx) {
+    var ml = util.widgets.make_menulist(
+        util.functional.map_list(
+            g.data.list.acnc,
+            function(o) {
+                return [ o.name(), o.id() ];
+            }
+        ),
+        typeof g.volumes[vol_idx].label_class() == 'object'
+            ? g.volumes[vol_idx].label_class().id()
+            : g.volumes[vol_idx].label_class()
+    );
+    ml.addEventListener(
+        'command',
+        function(ev) {
+            g.volumes[vol_idx].ischanged(1);
+            g.volumes[vol_idx].label_class(ml.value);
+        },
+        false
+    );
+    return ml;
+}
+
+g.render_prefix_menu = function(vol_idx) {
+    var org = typeof g.volumes[vol_idx].owning_lib() == 'object'
+        ? g.volumes[vol_idx].owning_lib()
+        : g.data.hash.aou[ g.volumes[vol_idx].owning_lib() ];
+    var menulist = document.createElement('menulist');
+        var menupopup = document.createElement('menupopup');
+        menulist.appendChild(menupopup);
+        var org_list = []; // order from top of consortium to owning lib
+        while(org) {
+            org_list.unshift(org.id());
+            org = org.parent_ou();
+            if (org && typeof org != 'object') {
+                org = g.data.hash.aou[ org ];
+            }
+        }
+        for (var i = 0; i < org_list.length; i++) {
+            g.render_prefix_menu_items(menupopup,org_list[i]);
+        }
+        menulist.setAttribute('value',
+            typeof g.volumes[vol_idx].prefix() == 'object'
+                ? g.volumes[vol_idx].prefix().id()
+                : g.volumes[vol_idx].prefix()
+        );
+
+    menulist.addEventListener(
+        'command',
+        function() {
+            g.volumes[vol_idx].ischanged(1);
+            g.volumes[vol_idx].prefix(menulist.value);
+        },
+        false
+    );
+    return menulist;
+}
+
+g.render_prefix_menu_items = function(menupopup,ou_id) {
+    if (typeof g.data.list['acnp_for_lib_'+ou_id] == 'undefined') {
+        g.data.list['acnp_for_lib_'+ou_id] = g.network.simple_request(
+            'FM_ACNP_RETRIEVE_VIA_PCRUD',
+            [ ses(), {"owning_lib":{"=":ou_id}}, {"order_by":{"acnp":"label_sortkey"}} ]
+        );
+        g.data.stash('list');
+    }
+    for (var i = 0; i < g.data.list['acnp_for_lib_'+ou_id].length; i++) {
+        var my_acnp = g.data.list['acnp_for_lib_'+ou_id][i];
+        var menuitem = document.createElement('menuitem');
+        menupopup.appendChild(menuitem);
+            menuitem.setAttribute(
+                'label',
+                my_acnp.id() == -1 ? '' :
+                $('catStrings').getFormattedString(
+                    'staff.cat.volume_copy_creator.call_number_prefix.menuitem_label',
+                    [
+                        my_acnp.label(),
+                        g.data.hash.aou[ ou_id ].shortname()
+                    ]
+                )
+            );
+            menuitem.setAttribute('value',my_acnp.id());
+    }
+}
+
+
+g.render_suffix_menu = function(vol_idx) {
+    var org = typeof g.volumes[vol_idx].owning_lib() == 'object'
+        ? g.volumes[vol_idx].owning_lib()
+        : g.data.hash.aou[ g.volumes[vol_idx].owning_lib() ];
+    var menulist = document.createElement('menulist');
+        var menupopup = document.createElement('menupopup');
+        menulist.appendChild(menupopup);
+        var org_list = []; // order from top of consortium to owning lib
+        while(org) {
+            org_list.unshift(org.id());
+            org = org.parent_ou();
+            if (org && typeof org != 'object') {
+                org = g.data.hash.aou[ org ];
+            }
+        }
+        for (var i = 0; i < org_list.length; i++) {
+            g.render_suffix_menu_items(menupopup,org_list[i]);
+        }
+        menulist.setAttribute('value',
+            typeof g.volumes[vol_idx].suffix() == 'object'
+                ? g.volumes[vol_idx].suffix().id()
+                : g.volumes[vol_idx].suffix()
+        );
+
+    menulist.addEventListener(
+        'command',
+        function() {
+            g.volumes[vol_idx].ischanged(1);
+            g.volumes[vol_idx].suffix(menulist.value);
+        },
+        false
+    );
+    return menulist;
+}
+
+g.render_suffix_menu_items = function(menupopup,ou_id) {
+    if (typeof g.data.list['acns_for_lib_'+ou_id] == 'undefined') {
+        g.data.list['acns_for_lib_'+ou_id] = g.network.simple_request(
+            'FM_ACNS_RETRIEVE_VIA_PCRUD',
+            [ ses(), {"owning_lib":{"=":ou_id}}, {"order_by":{"acns":"label_sortkey"}} ]
+        );
+        g.data.stash('list');
+    }
+    for (var i = 0; i < g.data.list['acns_for_lib_'+ou_id].length; i++) {
+        var my_acns = g.data.list['acns_for_lib_'+ou_id][i];
+        var menuitem = document.createElement('menuitem');
+        menupopup.appendChild(menuitem);
+            menuitem.setAttribute(
+                'label',
+                my_acns.id() == -1 ? '' :
+                $('catStrings').getFormattedString(
+                    'staff.cat.volume_copy_creator.call_number_suffix.menuitem_label',
+                    [
+                        my_acns.label(),
+                        g.data.hash.aou[ ou_id ].shortname()
+                    ]
+                )
+            );
+            menuitem.setAttribute('value',my_acns.id());
+    }
+}
+
+

Modified: trunk/Open-ILS/xul/staff_client/server/cat/volume_editor.xul
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/cat/volume_editor.xul	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/server/cat/volume_editor.xul	2011-03-29 15:24:44 UTC (rev 19883)
@@ -19,7 +19,7 @@
 
 <window id="cat_volume_editor_win" 
     onload="try { my_init(); font_helper(); persist_helper(); } catch(E) { alert(E); }"
-    title="&staff.cat.volume_editor.title;" height="400" width="300" oils_persist="height width"
+    title="&staff.cat.volume_editor.title;" height="800" width="300" oils_persist="height width"
     xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
     <!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
@@ -30,76 +30,8 @@
         <scripts id="openils_util_scripts"/>
 
     <script type="text/javascript" src="/xul/server/main/JSAN.js"/>
-    <script>
-    <![CDATA[
+    <script type="text/javascript" src="volume_editor.js"/>
 
-        var xulG = {};
-
-        function my_init() {
-            try {
-                netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-                if (typeof JSAN == 'undefined') { throw( $("commonStrings").getString('common.jsan.missing') ); }
-                JSAN.errorLevel = "die"; // none, warn, or die
-                JSAN.addRepository('/xul/server/');
-                JSAN.use('util.error'); g.error = new util.error();
-                g.error.sdump('D_TRACE','my_init() for cat/volume_editor.xul');
-
-                JSAN.use('OpenILS.data'); g.data = new OpenILS.data(); g.data.init({'via':'stash'});
-
-                JSAN.use('util.functional');
-
-                g.volumes = xul_param('volumes',{'stash_name':'volumes_temp','clear_xpcom':true,'modal_xulG':true}); //JSON2js( g.data.volumes_temp );
-                //g.data.volumes_temp = ''; g.data.stash('volumes_temp');
-
-                var rows = document.getElementById('rows');
-
-                var first_tb;
-
-                for (var i = 0; i < g.volumes.length; i++) {
-                    var row = document.createElement('row'); rows.appendChild(row);
-                    var lib_label = document.createElement('label'); row.appendChild(lib_label);
-                    var tb = document.createElement('textbox'); row.appendChild(tb);
-                    if (!first_tb) { first_tb = tb; }
-
-                    var lib_id = g.volumes[i].owning_lib();
-                    var last_lib_seen;
-
-                    if (last_lib_seen != lib_id ) {
-                        lib_label.setAttribute('value',g.data.hash.aou[ lib_id ].shortname() );
-                        last_lib_seen = lib_id;
-                    }
-
-                    tb.setAttribute('value',g.volumes[i].label());
-                    tb.setAttribute('onchange','try { var v = g.volumes['+i+']; v.ischanged("1"); v.label( this.value ); } catch(E) { alert(E); }');
-                }
-
-                first_tb.select(); first_tb.focus();
-
-            } catch(E) {
-                var err_msg = $("commonStrings").getFormattedString('common.exception', ['cat/volume_editor.xul', E]);
-                try { g.error.sdump('D_ERROR',err_msg); } catch(E) { dump(err_msg); dump(js2JSON(E)); }
-                alert(err_msg);
-            }
-        }
-
-        g.stash_and_close = function() {
-            try {
-                //g.data.volumes_temp = js2JSON( g.volumes );
-                //g.error.sdump('D_CAT','in modal window, g.data.volumes_temp = \n' + g.data.volumes_temp + '\n');
-                //g.data.stash('volumes_temp');
-                xulG.volumes = g.volumes;
-                xulG.update_these_volumes = 1;
-                xulG.auto_merge = document.getElementById('auto_merge').checked;
-                update_modal_xulG(xulG);
-                window.close();
-            } catch(E) {
-                alert('FIXME: volume editor -> ' + E);
-            }
-        }
-
-    ]]>
-    </script>
-    
     <messagecatalog id="catStrings" src="/xul/server/locale/<!--#echo var='locale'-->/cat.properties" />
     <messagecatalog id="circStrings" src="/xul/server/locale/<!--#echo var='locale'-->/circ.properties" />
 
@@ -115,8 +47,22 @@
             <checkbox id="auto_merge" label="&staff.cat.volume_editor.automerge.label;" accesskey="&staff.cat.volume_editor.automerge.accesskey;" oils_persist="checked"/>
         </hbox>
         <grid flex="1">
-            <columns> <column /> <column /> <column flex="1"/> </columns>
-            <rows id="rows" />
+            <columns>
+                <column />
+                <column />
+                <column />
+                <column flex="1"/>
+                <column />
+            </columns>
+            <rows id="rows">
+                <row>
+                    <label value="&staff.cat.volume_editor.owning_lib;" class="header"/>
+                    <label value="&staff.cat.volume_editor.classification;" class="header"/>
+                    <label value="&staff.cat.volume_editor.prefix;" class="header"/>
+                    <label value="&staff.cat.volume_editor.label;" class="header"/>
+                    <label value="&staff.cat.volume_editor.suffix;" class="header"/>
+                </row>
+            </rows>
         </grid>
     </groupbox>
 

Modified: trunk/Open-ILS/xul/staff_client/server/circ/copy_status.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/circ/copy_status.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/server/circ/copy_status.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -532,8 +532,10 @@
     
                                     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 w = xulG.new_tab(
-                                        window.xulG.url_prefix(urls.XUL_VOLUME_COPY_CREATOR),
+                                        url,
                                         { 'tab_name' : title },
                                         { 'doc_id' : r, 'ou_ids' : list, 'copy_shortcut' : copy_shortcut[r] }
                                     );
@@ -682,8 +684,10 @@
 
                                     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 w = xulG.new_tab(
-                                        window.xulG.url_prefix(urls.XUL_VOLUME_COPY_CREATOR),
+                                        url,
                                         { 'tab_name' : title },
                                         { 'doc_id' : r, 'ou_ids' : list }
                                     );

Modified: trunk/Open-ILS/xul/staff_client/server/circ/util.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/circ/util.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/server/circ/util.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -571,22 +571,45 @@
             'flex' : 1,
             'primary' : false,
             'hidden' : true,
-            'editable' : false, 'render' : function(my) {
-                if (my.acp && my.acp.call_number() == -1) {
+            'editable' : false, 'render' : function(my,scratch_data) {
+                var acn_id;
+                if (my.acn) {
+                    if (typeof my.acn == 'object') {
+                        acn_id = my.acn.id();
+                    } else {
+                        acn_id = my.acn;
+                    }
+                } else if (my.acp) {
+                    if (typeof my.acp.call_number() == 'object') {
+                        acn_id = my.acp.call_number().id();
+                    } else {
+                        acn_id = my.acp.call_number();
+                    }
+                }
+                if (!acn_id && acn_id != 0) {
+                    return '';
+                } else if (acn_id == -1) {
                     return document.getElementById('circStrings').getString('staff.circ.utils.not_cataloged');
-                } else if (my.acp && my.acp.call_number() == -2) {
+                } else if (acn_id == -2) {
                     return document.getElementById('circStrings').getString('staff.circ.utils.retrieving');
                 } else {
                     if (!my.acn) {
-                        var x = network.simple_request("FM_ACN_RETRIEVE.authoritative",[ my.acp.call_number() ]);
-                        if (x.ilsevent) {
-                            return document.getElementById('circStrings').getString('staff.circ.utils.not_cataloged');
+                        if (typeof scratch_data['acn_map'] == 'undefined') {
+                            scratch_data['acn_map'] = {};
+                        }
+                        if (typeof scratch_data['acn_map'][ acn_id ] == 'undefined') {
+                            var x = network.simple_request("FM_ACN_RETRIEVE.authoritative",[ acn_id ]);
+                            if (x.ilsevent) {
+                                return document.getElementById('circStrings').getString('staff.circ.utils.not_cataloged');
+                            } else {
+                                my.acn = x;
+                                scratch_data['acn_map'][ acn_id ] = my.acn;
+                            }
                         } else {
-                            my.acn = x; return x.label();
+                            my.acn = scratch_data['acn_map'][ acn_id ];
                         }
-                    } else {
-                        return my.acn.label();
                     }
+                    return my.acn.label();
                 }
             },
             'persist' : 'hidden width ordinal'
@@ -608,6 +631,71 @@
             'persist' : 'hidden width ordinal'
         },
         {
+            'id' : 'prefix',
+            'fm_class' : 'acn',
+            'label' : document.getElementById('circStrings').getString('staff.circ.utils.prefix'),
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : true,
+            'editable' : false, 'render' : function(my) {
+                if (typeof my.acn == 'undefined') return '';
+                return (typeof my.acn.prefix() == 'object') ? my.acn.prefix().label() : my.acn.prefix();
+            },
+            'persist' : 'hidden width ordinal'
+        },
+        {
+            'id' : 'suffix',
+            'fm_class' : 'acn',
+            'label' : document.getElementById('circStrings').getString('staff.circ.utils.suffix'),
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : true,
+            'editable' : false, 'render' : function(my) {
+                if (typeof my.acn == 'undefined') return '';
+                return (typeof my.acn.suffix() == 'object') ? my.acn.suffix().label() : my.acn.suffix();
+            },
+            'persist' : 'hidden width ordinal'
+        },
+        {
+            'id' : 'label_class',
+            'fm_class' : 'acn',
+            'label' : document.getElementById('circStrings').getString('staff.circ.utils.label_class'),
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : true,
+            'editable' : false, 'render' : function(my) {
+                if (typeof my.acn == 'undefined') return '';
+                return (typeof my.acn.label_class() == 'object') ? my.acn.label_class().name() : my.acn.label_class();
+            },
+            'persist' : 'hidden width ordinal'
+        },
+        {
+            'id' : 'parts',
+            'fm_class' : 'acp',
+            'label' : document.getElementById('commonStrings').getString('staff.acp_label_parts'),
+            'flex' : 1,
+            'sort_type' : 'number',
+            'primary' : false,
+            'hidden' : true,
+            'editable' : false, 'render' : function(my) {
+                if (! my.acp.parts()) return '';
+                var parts = my.acp.parts();
+                var display_string = '';
+                for (var i = 0; i < parts.length; i++) {
+                    if (my.doc_id) {
+                        if (my.doc_id == parts[i].record()) {
+                            return parts[i].label();
+                        }
+                    } else {
+                        if (i != 0) display_string += ' : ';
+                        display_string += parts[i].label();
+                    }
+                }
+                return display_string;
+            },
+            'persist' : 'hidden width ordinal'
+        },
+        {
             'id' : 'copy_number',
             'fm_class' : 'acp',
             'label' : document.getElementById('commonStrings').getString('staff.acp_label_copy_number'),
@@ -2233,9 +2321,11 @@
 };
 */
 circ.util.std_map_row_to_columns = function(error_value) {
-    return function(row,cols) {
+    return function(row,cols,scratch) {
         // row contains { 'my' : { 'acp' : {}, 'circ' : {}, 'mvr' : {} } }
         // cols contains all of the objects listed above in columns
+        // scratch is a temporary space shared by all cells/rows (or just per row if not explicitly passed in)
+        if (!scratch) { scratch = {}; }
 
         var obj = {};
         JSAN.use('util.error'); obj.error = new util.error();
@@ -2249,7 +2339,7 @@
         try {
             for (var i = 0; i < cols.length; i++) {
                 switch (typeof cols[i].render) {
-                    case 'function': try { values[i] = cols[i].render(my); } catch(E) { values[i] = error_value; obj.error.sdump('D_COLUMN_RENDER_ERROR',E); } break;
+                    case 'function': try { values[i] = cols[i].render(my,scratch); } catch(E) { values[i] = error_value; obj.error.sdump('D_COLUMN_RENDER_ERROR',E); } break;
                     case 'string' : cmd += 'try { ' + cols[i].render + '; values['+i+'] = v; } catch(E) { values['+i+'] = error_value; }'; break;
                     default: cmd += 'values['+i+'] = "??? '+(typeof cols[i].render)+'"; ';
                 }

Modified: trunk/Open-ILS/xul/staff_client/server/locale/en-US/cat.properties
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/locale/en-US/cat.properties	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/server/locale/en-US/cat.properties	2011-03-29 15:24:44 UTC (rev 19883)
@@ -44,7 +44,6 @@
 staff.cat.copy_browser.delete_volume.cancel=Cancel
 staff.cat.copy_browser.delete_volume.override=Override Delete Failure?
 staff.cat.copy_browser.delete_volume.copies_remain=You must delete all the copies on the volume before you may delete the volume itself.
-staff.cat.copy_browser.delete_volume.success=Volumes deleted.
 staff.cat.copy_browser.delete_volume.exception=copy browser -> delete volumes
 staff.cat.copy_browser.mark_library.alert=Library + Record marked as Volume Transfer Destination
 staff.cat.copy_browser.mark_library.prompt=Choose just one Library to mark as Volume Transfer Destination
@@ -392,19 +391,26 @@
 staff.cat.volume_buckets.window_tab_name=Volume Buckets
 staff.cat.volume_copy_creator.my_init.btn.label=Apply
 staff.cat.volume_copy_creator.my_init.btn.accesskey=A
+staff.cat.volume_copy_creator.create_part.btn.label=Create Part Designator
 staff.cat.volume_copy_creator.edit_then_create.btn.label=Edit then Create
 staff.cat.volume_copy_creator.edit_then_create.btn.accesskey=C
 staff.cat.volume_copy_creator.create_with_defaults.btn.label=Create with Defaults
 staff.cat.volume_copy_creator.create_with_defaults.btn.accesskey=D
+staff.cat.volume_copy_creator.create.btn.label=Create Volumes/Items
+staff.cat.volume_copy_creator.create.btn.accesskey=C
 staff.cat.volume_copy_creator.edit_then_rebarcode.btn.label=Edit then Re-barcode
 staff.cat.volume_copy_creator.edit_then_rebarcode.btn.accesskey=E
-staff.cat.volume_copy_creator.rebarcode.btn.label=Re-barcode
+staff.cat.volume_copy_creator.rebarcode.btn.label=Re-barcode / Update Items
 staff.cat.volume_copy_creator.rebarcode.btn.accesskey=R
 staff.cat.volume_copy_creator.render_volume_count_entry.message=You may not add more than %1$s items at a time for a given volume in this interface.
 staff.cat.volume_copy_creator.render_volume_count_entry.title=Maximum items exceeded.
 staff.cat.volume_copy_creator.render_volume_count_entry.ok_label=Ok
+staff.cat.volume_copy_creator.render_callnumber_copy_count_entry.classification=Classification
+staff.cat.volume_copy_creator.render_callnumber_copy_count_entry.prefix=Prefix
 staff.cat.volume_copy_creator.render_callnumber_copy_count_entry.call_nums=Call Numbers
+staff.cat.volume_copy_creator.render_callnumber_copy_count_entry.suffix=Suffix
 staff.cat.volume_copy_creator.render_callnumber_copy_count_entry.num_of_copies=# of Copies
+staff.cat.volume_copy_creator.render_callnumber_copy_count_entry.barcodes_and_parts=Barcodes / Part Designation
 staff.cat.volume_copy_creator.render_barcode_entry.alert_message="%1$s" is an invalid barcode.
 staff.cat.volume_copy_creator.render_barcode_entry.alert_title=Invalid Barcode
 staff.cat.volume_copy_creator.render_barcode_entry.alert_ok_button=OK
@@ -414,6 +420,10 @@
 staff.cat.volume_copy_creator.stash_and_close.tree_err3=volume tree update 3
 staff.cat.volume_copy_creator.load_prefs.err_retrieving_prefs=Error retrieving stored preferences
 staff.cat.volume_copy_creator.save_prefs.err_storing_prefs=Error storing preferences
+# %1$s = Call Number Prefix Label, %2$s = Call Number Prefix Owning Lib Shortname
+staff.cat.volume_copy_creator.call_number_prefix.menuitem_label=%2$s : %1$s
+# %1$s = Call Number Suffix Label, %2$s = Call Number Suffix Owning Lib Shortname
+staff.cat.volume_copy_creator.call_number_suffix.menuitem_label=%2$s : %1$s
 staff.cat.z3950.native_catalog=Native Catalog
 staff.cat.z3950.obj_list_init.list_construction_error=Failure during list construction.
 staff.cat.z3950.results_view.label=Results View

Modified: trunk/Open-ILS/xul/staff_client/server/locale/en-US/circ.properties
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/locale/en-US/circ.properties	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/server/locale/en-US/circ.properties	2011-03-29 15:24:44 UTC (rev 19883)
@@ -238,6 +238,9 @@
 staff.circ.utils.not_cataloged=Not Cataloged
 staff.circ.utils.retrieving=Retrieving...
 staff.circ.utils.owning_lib=Owning Library
+staff.circ.utils.prefix=Prefix
+staff.circ.utils.suffix=Suffix
+staff.circ.utils.label_class=Classification
 staff.circ.utils.loan_duration.short=Short
 staff.circ.utils.loan_duration.normal=Normal
 staff.circ.utils.loan_duration.long=Long

Modified: trunk/Open-ILS/xul/staff_client/server/locale/en-US/common.properties
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/locale/en-US/common.properties	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/server/locale/en-US/common.properties	2011-03-29 15:24:44 UTC (rev 19883)
@@ -37,6 +37,7 @@
 # %1$s = circ modifier code, %2$s = circ modifier name, %3$s = circ modifier description
 staff.circ_modifier.display=%1$s : %2$s : %3$s
 staff.acp_label_copy_number=Copy Number
+staff.acp_label_parts=Part
 staff.acp_label_deposit_amount=Deposit Amount
 staff.acp_label_fine_level=Fine Level
 staff.acp_label_id=Copy ID

Modified: trunk/Open-ILS/xul/staff_client/server/patron/util.js
===================================================================
--- trunk/Open-ILS/xul/staff_client/server/patron/util.js	2011-03-29 02:09:14 UTC (rev 19882)
+++ trunk/Open-ILS/xul/staff_client/server/patron/util.js	2011-03-29 15:24:44 UTC (rev 19883)
@@ -540,9 +540,10 @@
 }
 
 patron.util.std_map_row_to_columns = function(error_value) {
-    return function(row,cols) {
+    return function(row,cols,scratch) {
         // row contains { 'my' : { 'au' : {} } }
         // cols contains all of the objects listed above in columns
+        // scratch is a temporary space shared by all cells/rows (or just per row if not explicitly passed in)
         
         var obj = {}; obj.OpenILS = {}; 
         JSAN.use('util.error'); obj.error = new util.error();



More information about the open-ils-commits mailing list