LP1794588 Web client edit single call number changes all when multiple items attached
[evergreen-equinox.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Cat.pm
index e1d2c82..3fcd255 100644 (file)
@@ -106,9 +106,10 @@ __PACKAGE__->register_method(
 );
 
 sub create_record_xml {
-    my( $self, $client, $login, $xml, $source ) = @_;
+    my( $self, $client, $login, $xml, $source, $oargs, $strip_grps ) = @_;
 
     my $override = 1 if $self->api_name =~ /override/;
+    $oargs = { all => 1 } unless defined $oargs;
 
     my( $user_obj, $evt ) = $U->checksesperm($login, 'CREATE_MARC');
     return $evt if $evt;
@@ -120,7 +121,7 @@ sub create_record_xml {
     $meth = $self->method_lookup(
         "open-ils.cat.biblio.record.xml.import.override") if $override;
 
-    my ($s) = $meth->run($login, $xml, $source);
+    my ($s) = $meth->run($login, $xml, $source, $oargs, $strip_grps);
     return $s;
 }
 
@@ -155,22 +156,23 @@ __PACKAGE__->register_method(
 );
 
 sub biblio_record_replace_marc  {
-    my( $self, $conn, $auth, $recid, $newxml, $source ) = @_;
+    my( $self, $conn, $auth, $recid, $newxml, $source, $oargs, $strip_grps ) = @_;
     my $e = new_editor(authtoken=>$auth, xact=>1);
     return $e->die_event unless $e->checkauth;
-    return $e->die_event unless $e->allowed('CREATE_MARC', $e->requestor->ws_ou);
+    return $e->die_event unless $e->allowed('UPDATE_MARC', $e->requestor->ws_ou);
 
     my $fix_tcn = $self->api_name =~ /replace/o;
-    my $override = $self->api_name =~ /override/o;
+    if($self->api_name =~ /override/o) {
+        $oargs = { all => 1 } unless defined $oargs;
+    } else {
+        $oargs = {};
+    }
 
     my $res = OpenILS::Application::Cat::BibCommon->biblio_record_replace_marc(
-        $e, $recid, $newxml, $source, $fix_tcn, $override);
+        $e, $recid, $newxml, $source, $fix_tcn, $oargs, $strip_grps);
 
     $e->commit unless $U->event_code($res);
 
-    #my $ses = OpenSRF::AppSession->create('open-ils.ingest');
-    #$ses->request('open-ils.ingest.full.biblio.record', $recid);
-
     return $res;
 }
 
@@ -261,11 +263,11 @@ sub template_overlay_container {
         $template = $e->retrieve_biblio_record_entry( $titem->target_biblio_record_entry )->marc;
     }
 
-    my $responses = [];
-    my $some_failed = 0;
+    my $num_failed = 0;
+    my $num_succeeded = 0;
 
-    $self->respond_complete(
-        $actor->request('open-ils.actor.anon_cache.set_value', $auth, res_list => $responses)->gather(1)
+    $conn->respond_complete(
+        $actor->request('open-ils.actor.anon_cache.set_value', $auth, batch_edit_progress => {})->gather(1)
     ) if ($actor);
 
     for my $item ( @$items ) {
@@ -279,11 +281,20 @@ sub template_overlay_container {
             )->[0]->{'vandelay.template_overlay_bib_record'};
         }
 
-        $some_failed++ if ($success eq 'f');
+        if ($success eq 'f') {
+            $num_failed++;
+        } else {
+            $num_succeeded++;
+        }
 
         if ($actor) {
-            push @$responses, { record => $rec->id, success => $success };
-            $actor->request('open-ils.actor.anon_cache.set_value', $auth, res_list => $responses);
+            $actor->request(
+                'open-ils.actor.anon_cache.set_value', $auth,
+                batch_edit_progress => {
+                    succeeded => $num_succeeded,
+                    failed    => $num_failed
+                },
+            );
         } else {
             $conn->respond({ record => $rec->id, success => $success });
         }
@@ -292,8 +303,15 @@ sub template_overlay_container {
             unless ($e->delete_container_biblio_record_entry_bucket_item($item)) {
                 $e->rollback;
                 if ($actor) {
-                    push @$responses, { complete => 1, success => 'f' };
-                    $actor->request('open-ils.actor.anon_cache.set_value', $auth, res_list => $responses);
+                    $actor->request(
+                        'open-ils.actor.anon_cache.set_value', $auth,
+                        batch_edit_progress => {
+                            complete => 1,
+                            success  => 'f',
+                            succeeded => $num_succeeded,
+                            failed    => $num_failed,
+                        }
+                    );
                     return undef;
                 } else {
                     return { complete => 1, success => 'f' };
@@ -302,21 +320,35 @@ sub template_overlay_container {
         }
     }
 
-    if ($titem && !$some_failed) {
+    if ($titem && !$num_failed) {
         return $e->die_event unless ($e->delete_container_biblio_record_entry_bucket_item($titem));
     }
 
     if ($e->commit) {
         if ($actor) {
-            push @$responses, { complete => 1, success => 't' };
-            $actor->request('open-ils.actor.anon_cache.set_value', $auth, res_list => $responses);
+            $actor->request(
+                'open-ils.actor.anon_cache.set_value', $auth,
+                batch_edit_progress => {
+                    complete => 1,
+                    success  => 't',
+                    succeeded => $num_succeeded,
+                    failed    => $num_failed,
+                }
+            );
         } else {
             return { complete => 1, success => 't' };
         }
     } else {
         if ($actor) {
-            push @$responses, { complete => 1, success => 'f' };
-            $actor->request('open-ils.actor.anon_cache.set_value', $auth, res_list => $responses);
+            $actor->request(
+                'open-ils.actor.anon_cache.set_value', $auth,
+                batch_edit_progress => {
+                    complete => 1,
+                    success  => 'f',
+                    succeeded => $num_succeeded,
+                    failed    => $num_failed,
+                }
+            );
         } else {
             return { complete => 1, success => 'f' };
         }
@@ -404,22 +436,23 @@ __PACKAGE__->register_method(
 
 
 sub biblio_record_xml_import {
-    my( $self, $client, $authtoken, $xml, $source, $auto_tcn) = @_;
+    my( $self, $client, $authtoken, $xml, $source, $auto_tcn, $oargs, $strip_grps) = @_;
     my $e = new_editor(xact=>1, authtoken=>$authtoken);
     return $e->die_event unless $e->checkauth;
     return $e->die_event unless $e->allowed('IMPORT_MARC', $e->requestor->ws_ou);
 
-    my $override = $self->api_name =~ /override/;
+    if ($self->api_name =~ /override/) {
+        $oargs = { all => 1 } unless defined $oargs;
+    } else {
+        $oargs = {};
+    }
     my $record = OpenILS::Application::Cat::BibCommon->biblio_record_xml_import(
-        $e, $xml, $source, $auto_tcn, $override);
+        $e, $xml, $source, $auto_tcn, $oargs, $strip_grps);
 
     return $record if $U->event_code($record);
 
     $e->commit;
 
-    #my $ses = OpenSRF::AppSession->create('open-ils.ingest');
-    #$ses->request('open-ils.ingest.full.biblio.record', $record->id);
-
     return $record;
 }
 
@@ -495,6 +528,7 @@ sub biblio_record_marc_cn {
         my $tag = substr($field, 0, 3);
         $logger->debug("Tag = $tag");
         my @node = $doc->findnodes("//marc:datafield[\@tag='$tag']");
+        next unless (@node);
 
         # Now parse the subfields and build up the subfield XPath
         my @subfields = split(//, substr($field, 3));
@@ -503,16 +537,17 @@ sub biblio_record_marc_cn {
         if (!@subfields) {
             @subfields = ('a');
         }
-        my $subxpath;
-        foreach my $sf (@subfields) {
-            $subxpath .= "\@code='$sf' or ";
-        }
-        $subxpath = substr($subxpath, 0, -4);
-        $logger->debug("subxpath = $subxpath");
+        my $xpath = 'marc:subfield[' . join(' or ', map { "\@code='$_'" } @subfields) . ']';
+        $logger->debug("xpath = $xpath");
 
         # Find the contents of the specified subfields
         foreach my $x (@node) {
-            my $cn = $x->findvalue("marc:subfield[$subxpath]");
+            # We can't use find($xpath)->to_literal_delimited here because older 2.x
+            # versions of the XML::LibXML module don't have to_literal_delimited().
+            my $cn = join(
+                ' ',
+                map { $_->textContent } $x->findnodes($xpath)
+            );
             push @res, {$tag => $cn} if ($cn);
         }
     }
@@ -637,14 +672,19 @@ sub retrieve_copies {
         @org_ids = ($user_obj->home_ou);
     }
 
+    # Create an editor that can be shared across all iterations of 
+    # _build_volume_list().  Otherwise, .authoritative calls can result 
+    # in creating too many cstore connections.
+    my $e = new_editor();
+
     if( $self->api_name =~ /global/ ) {
-        return _build_volume_list( { record => $docid, deleted => 'f', label => { '<>' => '##URI##' } } );
+        return _build_volume_list($e, { record => $docid, deleted => 'f', label => { '<>' => '##URI##' } } );
 
     } else {
 
         my @all_vols;
         for my $orgid (@org_ids) {
-            my $vols = _build_volume_list( 
+            my $vols = _build_volume_list($e,
                     { record => $docid, owning_lib => $orgid, deleted => 'f', label => { '<>' => '##URI##' } } );
             push( @all_vols, @$vols );
         }
@@ -657,10 +697,12 @@ sub retrieve_copies {
 
 
 sub _build_volume_list {
+    my $e = shift;
     my $search_hash = shift;
 
+    $e ||= new_editor();
+
     $search_hash->{deleted} = 'f';
-    my $e = new_editor();
 
     my $vols = $e->search_asset_call_number([
         $search_hash,
@@ -677,11 +719,25 @@ sub _build_volume_list {
 
         my $copies = $e->search_asset_copy([
             { call_number => $volume->id , deleted => 'f' },
-            { flesh => 1, flesh_fields => { acp => ['stat_cat_entries','parts'] } }
+            {
+                join => {
+                    acpm => {
+                        type => 'left',
+                        join => {
+                            bmp => { type => 'left' }
+                        }
+                    }
+                },
+                flesh => 1,
+                flesh_fields => { acp => ['stat_cat_entries','parts'] },
+                order_by => [
+                    {'class' => 'bmp', 'field' => 'label_sortkey', 'transform' => 'oils_text_as_bytea'},
+                    {'class' => 'bmp', 'field' => 'label', 'transform' => 'oils_text_as_bytea'},
+                    {'class' => 'acp', 'field' => 'barcode'}
+                ]
+            }
         ]);
 
-        $copies = [ sort { $a->barcode cmp $b->barcode } @$copies  ];
-
         for my $c (@$copies) {
             if( $c->status == OILS_COPY_STATUS_CHECKED_OUT ) {
                 $c->circulations(
@@ -718,15 +774,19 @@ __PACKAGE__->register_method(
 
 
 sub fleshed_copy_update {
-    my( $self, $conn, $auth, $copies, $delete_stats ) = @_;
+    my( $self, $conn, $auth, $copies, $delete_stats, $oargs, $create_parts ) = @_;
     return 1 unless ref $copies;
     my( $reqr, $evt ) = $U->checkses($auth);
     return $evt if $evt;
     my $editor = new_editor(requestor => $reqr, xact => 1);
-    my $override = $self->api_name =~ /override/;
+    if ($self->api_name =~ /override/) {
+        $oargs = { all => 1 } unless defined $oargs;
+    } else {
+        $oargs = {};
+    }
     my $retarget_holds = [];
     $evt = OpenILS::Application::Cat::AssetCommon->update_fleshed_copies(
-        $editor, $override, undef, $copies, $delete_stats, $retarget_holds, undef);
+        $editor, $oargs, undef, $copies, $delete_stats, $retarget_holds, undef, $create_parts);
 
     if( $evt ) { 
         $logger->info("fleshed copy update failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
@@ -749,6 +809,112 @@ sub reset_hold_list {
     $ses->request('open-ils.circ.hold.reset.batch', $auth, $hold_ids);
 }
 
+__PACKAGE__->register_method(
+    method    => "transfer_copies_to_volume",
+    api_name  => "open-ils.cat.transfer_copies_to_volume",
+    argc      => 3,
+    signature => {
+        desc   => 'Transfers specified copies to the specified call number, and changes Circ Lib to match the new Owning Lib.',
+        params => [
+            {desc => 'Authtoken', type => 'string'},
+            {desc => 'Call Number ID', type => 'number'},
+            {desc => 'Array of Copy IDs', type => 'array'},
+        ]
+    },
+    return => {desc => '1 on success, Event on error'}
+);
+
+__PACKAGE__->register_method(
+    method   => "transfer_copies_to_volume",
+    api_name => "open-ils.cat.transfer_copies_to_volume.override",);
+
+sub transfer_copies_to_volume {
+    my( $self, $conn, $auth, $volume, $copies, $oargs ) = @_;
+    my $delete_stats = 1;
+    my $force_delete_empty_bib = undef;
+    my $create_parts = undef;
+
+    # initial tests
+
+    return 1 unless ref $copies;
+    my( $reqr, $evt ) = $U->checkses($auth);
+    return $evt if $evt;
+    my $editor = new_editor(requestor => $reqr, xact => 1);
+    if ($self->api_name =~ /override/) {
+        $oargs = { all => 1 } unless defined $oargs;
+    } else {
+        $oargs = {};
+    }
+
+    # does the volume exist?  good, we also need its owning_lib later
+    my( $cn, $cn_evt ) = $U->fetch_callnumber( $volume, 0, $editor );
+    return $cn_evt if $cn_evt;
+
+    # flesh and munge the copies
+    my $fleshed_copies = [];
+    my $copy;
+    foreach my $copy_id ( @{ $copies } ) {
+        $copy = $editor->search_asset_copy([
+            { id => $copy_id , deleted => 'f' },
+            {
+                flesh => 1,
+                flesh_fields => { acp => ['parts', 'stat_cat_entries'] }
+            }
+        ])->[0];
+        return OpenILS::Event->new('ASSET_COPY_NOT_FOUND') if !$copy;
+        $copy->call_number( $volume );
+        $copy->circ_lib( $cn->owning_lib() );
+        $copy->ischanged( 't' );
+        push @$fleshed_copies, $copy;
+    }
+
+    # actual work
+    my $retarget_holds = [];
+    $evt = OpenILS::Application::Cat::AssetCommon->update_fleshed_copies(
+        $editor, $oargs, undef, $fleshed_copies, $delete_stats, $retarget_holds, $force_delete_empty_bib, $create_parts);
+
+    if( $evt ) { 
+        $logger->info("copy to volume transfer failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
+        $editor->rollback; 
+        return $evt; 
+    }
+
+    # take care of the parts
+    for my $copy (@$fleshed_copies) {
+        my $parts = $copy->parts;
+        next unless $parts;
+        my $part_objs = [];
+        foreach my $part (@$parts) {
+            my $part_label = $part->label;
+            my $part_obj = $editor->search_biblio_monograph_part(
+              {
+                   label=>$part_label,
+                   record=>$cn->record,
+                   deleted=>'f'
+              }
+           )->[0];
+           if (!$part_obj) {
+               $part_obj = Fieldmapper::biblio::monograph_part->new();
+               $part_obj->label( $part_label );
+               $part_obj->record( $cn->record );
+               unless($editor->create_biblio_monograph_part($part_obj)) {
+                 return $editor->die_event if $editor->die_event;
+               }
+           }
+           push @$part_objs, $part_obj;
+        }
+        $copy->parts( $part_objs );
+        $copy->ischanged(1);
+        $evt = OpenILS::Application::Cat::AssetCommon->update_copy_parts($editor, $copy, 1); #delete_parts=1
+        return $evt if $evt;
+    }
+
+    $editor->commit;
+    $logger->info("copy to volume transfer successfully updated ".scalar(@$copies)." copies");
+    reset_hold_list($auth, $retarget_holds);
+
+    return 1;
+}
 
 __PACKAGE__->register_method(
     method    => 'in_db_merge',
@@ -837,6 +1003,122 @@ sub in_db_auth_merge {
 }
 
 __PACKAGE__->register_method(
+    method    => 'calculate_marc_merge',
+    api_name  => 'open-ils.cat.merge.marc.per_profile',
+    signature => q/
+        Calculate the result of merging one or more MARC records
+        per the specified merge profile
+        @param auth The login session key
+        @param merge_profile ID of the record merge profile
+        @param records Array of two or more MARCXML records to be
+                       merged. If two are supplied, the first
+                       is treated as the record to be overlaid,
+                       and the the incoming record that will
+                       overlay the first. If more than two are
+                       supplied, the first is treated as the
+                       record to be overlaid, and each following
+                       record in turn will be merged into that
+                       record.
+        @return MARCXML string of the results of the merge
+    /
+);
+__PACKAGE__->register_method(
+    method    => 'calculate_bib_marc_merge',
+    api_name  => 'open-ils.cat.merge.biblio.per_profile',
+    signature => q/
+        Calculate the result of merging one or more bib records
+        per the specified merge profile
+        @param auth The login session key
+        @param merge_profile ID of the record merge profile
+        @param records Array of two or more bib record IDs of
+                       the bibs to be merged.
+        @return MARCXML string of the results of the merge
+    /
+);
+__PACKAGE__->register_method(
+    method    => 'calculate_authority_marc_merge',
+    api_name  => 'open-ils.cat.merge.authority.per_profile',
+    signature => q/
+        Calculate the result of merging one or more authority records
+        per the specified merge profile
+        @param auth The login session key
+        @param merge_profile ID of the record merge profile
+        @param records Array of two or more bib record IDs of
+                       the bibs to be merged.
+        @return MARCXML string of the results of the merge
+    /
+);
+
+sub _handle_marc_merge {
+    my ($e, $merge_profile_id, $records) = @_;
+
+    my $result = shift @$records;
+    foreach my $incoming (@$records) {
+        my $response = $e->json_query({
+            from => [
+                'vandelay.merge_record_xml_using_profile',
+                $incoming, $result,
+                $merge_profile_id
+            ]
+        });
+        return unless ref($response);
+        $result = $response->[0]->{'vandelay.merge_record_xml_using_profile'};
+    }
+    return $result;
+}
+
+sub calculate_marc_merge {
+    my( $self, $conn, $auth, $merge_profile_id, $records ) = @_;
+
+    my $e = new_editor(authtoken=>$auth, xact=>1);
+    return $e->die_event unless $e->checkauth;
+
+    my $merge_profile = $e->retrieve_vandelay_merge_profile($merge_profile_id)
+        or return $e->die_event;
+    return $e->die_event unless ref($records) && @$records >= 2;
+
+    return _handle_marc_merge($e, $merge_profile_id, $records)
+}
+
+sub calculate_bib_marc_merge {
+    my( $self, $conn, $auth, $merge_profile_id, $bib_ids ) = @_;
+
+    my $e = new_editor(authtoken=>$auth, xact=>1);
+    return $e->die_event unless $e->checkauth;
+
+    my $merge_profile = $e->retrieve_vandelay_merge_profile($merge_profile_id)
+        or return $e->die_event;
+    return $e->die_event unless ref($bib_ids) && @$bib_ids >= 2;
+
+    my $records = [];
+    foreach my $id (@$bib_ids) {
+        my $bre = $e->retrieve_biblio_record_entry($id) or return $e->die_event;
+        push @$records, $bre->marc();
+    }
+
+    return _handle_marc_merge($e, $merge_profile_id, $records)
+}
+
+sub calculate_authority_marc_merge {
+    my( $self, $conn, $auth, $merge_profile_id, $authority_ids ) = @_;
+
+    my $e = new_editor(authtoken=>$auth, xact=>1);
+    return $e->die_event unless $e->checkauth;
+
+    my $merge_profile = $e->retrieve_vandelay_merge_profile($merge_profile_id)
+        or return $e->die_event;
+    return $e->die_event unless ref($authority_ids) && @$authority_ids >= 2;
+
+    my $records = [];
+    foreach my $id (@$authority_ids) {
+        my $are = $e->retrieve_authority_record_entry($id) or return $e->die_event;
+        push @$records, $are->marc();
+    }
+
+    return _handle_marc_merge($e, $merge_profile_id, $records)
+}
+
+__PACKAGE__->register_method(
     method   => "fleshed_volume_update",
     api_name => "open-ils.cat.asset.volume.fleshed.batch.update",);
 
@@ -845,15 +1127,21 @@ __PACKAGE__->register_method(
     api_name => "open-ils.cat.asset.volume.fleshed.batch.update.override",);
 
 sub fleshed_volume_update {
-    my( $self, $conn, $auth, $volumes, $delete_stats, $options ) = @_;
+    my( $self, $conn, $auth, $volumes, $delete_stats, $options, $oargs ) = @_;
     my( $reqr, $evt ) = $U->checkses($auth);
     return $evt if $evt;
     $options ||= {};
 
-    my $override = ($self->api_name =~ /override/);
+    if ($self->api_name =~ /override/) {
+        $oargs = { all => 1 } unless defined $oargs;
+    } else {
+        $oargs = {};
+    }
     my $editor = new_editor( requestor => $reqr, xact => 1 );
     my $retarget_holds = [];
     my $auto_merge_vols = $options->{auto_merge_vols};
+    my $create_parts = $options->{create_parts};
+    my $copy_ids = [];
 
     for my $vol (@$volumes) {
         $logger->info("vol-update: investigating volume ".$vol->id);
@@ -873,7 +1161,7 @@ sub fleshed_volume_update {
             return $editor->die_event unless
                 $editor->allowed('UPDATE_VOLUME', $vol->owning_lib);
 
-            if(my $evt = $assetcom->delete_volume($editor, $vol, $override, $$options{force_delete_copies})) {
+            if(my $evt = $assetcom->delete_volume($editor, $vol, $oargs, $$options{force_delete_copies})) {
                 $editor->rollback;
                 return $evt;
             }
@@ -883,28 +1171,74 @@ sub fleshed_volume_update {
 
         } elsif( $vol->isnew ) {
             $logger->info("vol-update: creating volume");
-            $evt = $assetcom->create_volume( $override, $editor, $vol );
+            ($vol,$evt) = $assetcom->create_volume( $auto_merge_vols ? { all => 1} : $oargs, $editor, $vol );
             return $evt if $evt;
 
         } elsif( $vol->ischanged ) {
             $logger->info("vol-update: update volume");
-            my $resp = update_volume($vol, $editor, ($override or $auto_merge_vols));
-            return $resp->{evt} if $resp->{evt};
-            $vol = $resp->{merge_vol};
+
+            # Three cases here:
+            #   1) We're editing a volume, and not its copies.
+            #   2) We're editing a volume, and a subset of its copies.
+            #   3) We're editing a volume, and all of its copies.
+            #
+            # For 1) and 3), we definitely want to edit the volume
+            # itself (and possibly auto-merge), but for 2), we want
+            # to create a new volume (and possibly auto-merge).
+
+            if (scalar(@$copies) == 0) { # case 1
+
+                my $resp = update_volume($vol, $editor, ($oargs->{all} or grep { $_ eq 'VOLUME_LABEL_EXISTS' } @{$oargs->{events}} or $auto_merge_vols));
+                return $resp->{evt} if $resp->{evt};
+                $vol = $resp->{merge_vol} if $resp->{merge_vol};
+
+            } else {
+
+                my $resp = $editor->json_query({
+                  select => {
+                    acp => [
+                      {transform => 'count', aggregate => 1, column => 'id', alias => 'count'}
+                    ]
+                  },
+                  from => 'acp',
+                  where => {
+                    call_number => $vol->id,
+                    deleted => 'f',
+                    id => {'not in' => [ map { $_->id } @$copies ]}
+                  }
+                });
+                if ($resp->[0]->{count} && $resp->[0]->{count} > 0) { # case 2
+
+                    ($vol,$evt) = $assetcom->create_volume( $auto_merge_vols ? { all => 1} : $oargs, $editor, $vol );
+                    return $evt if $evt;
+
+                } else { # case 3
+
+                    my $resp = update_volume($vol, $editor, ($oargs->{all} or grep { $_ eq 'VOLUME_LABEL_EXISTS' } @{$oargs->{events}} or $auto_merge_vols));
+                    return $resp->{evt} if $resp->{evt};
+                    $vol = $resp->{merge_vol} if $resp->{merge_vol};
+                }
+
+            }
         }
 
         # now update any attached copies
         if( $copies and @$copies and !$vol->isdeleted ) {
             $_->call_number($vol->id) for @$copies;
             $evt = $assetcom->update_fleshed_copies(
-                $editor, $override, $vol, $copies, $delete_stats, $retarget_holds, undef);
+                $editor, $oargs, $vol, $copies, $delete_stats, $retarget_holds, undef, $create_parts);
             return $evt if $evt;
+            push( @$copy_ids, $_->id ) for @$copies;
         }
     }
 
     $editor->finish;
     reset_hold_list($auth, $retarget_holds);
-    return scalar(@$volumes);
+    if ($options->{return_copy_ids}) {
+        return $copy_ids;
+    } else {
+        return scalar(@$volumes);
+    }
 }
 
 
@@ -982,7 +1316,7 @@ __PACKAGE__->register_method (
 
 
 sub batch_volume_transfer {
-    my( $self, $conn, $auth, $args ) = @_;
+    my( $self, $conn, $auth, $args, $oargs ) = @_;
 
     my $evt;
     my $rec     = $$args{docid};
@@ -990,6 +1324,7 @@ sub batch_volume_transfer {
     my $vol_ids = $$args{volumes};
 
     my $override = 1 if $self->api_name =~ /override/;
+    $oargs = { all => 1 } unless defined $oargs;
 
     $logger->info("merge: transferring volumes to lib=$o_lib and record=$rec");
 
@@ -1026,7 +1361,7 @@ sub batch_volume_transfer {
         # for each volume, see if there are any copies that have a 
         # remote circ_lib (circ_lib != vol->owning_lib and != $o_lib ).  
         # if so, warn them
-        unless( $override ) {
+        unless( $override && ($oargs->{all} || grep { $_ eq 'COPY_REMOTE_CIRC_LIB' } @{$oargs->{events}}) ) {
             for my $v (@all) {
 
                 $logger->debug("merge: searching for copies with remote circ_lib for volume ".$v->id);
@@ -1044,6 +1379,9 @@ sub batch_volume_transfer {
             }
         }
 
+        # record the difference between the destination bib and the present bib
+        my $same_bib = $vol->record == $rec;
+
         # see if there is a volume at the destination lib that 
         # already has the requested label
         my $existing_vol = $e->search_asset_call_number(
@@ -1095,7 +1433,13 @@ sub batch_volume_transfer {
 
         # regardless of what volume was used as the destination, 
         # update any copies that have moved over to the new lib
-        my $copies = $e->search_asset_copy({call_number=>$vol->id, deleted => 'f'});
+        my $copies = $e->search_asset_copy([
+            { call_number => $vol->id , deleted => 'f' },
+            {
+                flesh => 1,
+                flesh_fields => { acp => ['parts'] }
+            }
+        ]);
 
         # update circ lib on the copies - make this a method flag?
         for my $copy (@$copies) {
@@ -1107,6 +1451,40 @@ sub batch_volume_transfer {
             $e->update_asset_copy($copy) or return $e->event;
         }
 
+        # update parts if volume is moving bib records
+        if( !$same_bib ) {
+            for my $copy (@$copies) {
+                my $parts = $copy->parts;
+                next unless $parts;
+                my $part_objs = [];
+                foreach my $part (@$parts) {
+                    my $part_label = $part->label;
+                    my $part_obj = $e->search_biblio_monograph_part(
+                       {
+                            label=>$part_label,
+                            record=>$rec,
+                            deleted=>'f'
+                       }
+                    )->[0];
+
+                    if (!$part_obj) {
+                        $part_obj = Fieldmapper::biblio::monograph_part->new();
+                        $part_obj->label( $part_label );
+                        $part_obj->record( $rec );
+                        unless($e->create_biblio_monograph_part($part_obj)) {
+                          return $e->die_event if $e->die_event;
+                        }
+                    }
+                    push @$part_objs, $part_obj;
+                }
+
+                $copy->parts( $part_objs );
+                $copy->ischanged(1);
+                $evt = OpenILS::Application::Cat::AssetCommon->update_copy_parts($e, $copy, 1); #delete_parts=1
+                return $evt if $evt;
+            }
+        }
+
         # Now see if any empty records need to be deleted after all of this
 
         for(@rec_ids) {
@@ -1157,9 +1535,10 @@ __PACKAGE__->register_method(
 );
 
 sub create_serial_record_xml {
-    my( $self, $client, $login, $source, $owning_lib, $record_id, $xml ) = @_;
+    my( $self, $client, $login, $source, $owning_lib, $record_id, $xml, $oargs ) = @_;
 
     my $override = 1 if $self->api_name =~ /override/; # not currently used
+    $oargs = { all => 1 } unless defined $oargs; # Not currently used, but here for consistency.
 
     my $e = new_editor(xact=>1, authtoken=>$login);
     return $e->die_event unless $e->checkauth;
@@ -1267,7 +1646,7 @@ sub acn_sms_msg {
     my($self, $conn, $auth, $org_id, $carrier, $number, $target_ids) = @_;
 
     my $sms_enable = $U->ou_ancestor_setting_value(
-        $org_id || $U->fetch_org_tree->id,
+        $org_id || $U->get_org_tree->id,
         'sms.enable'
     );
     # We could maybe make a Validator for this on the templates
@@ -1276,7 +1655,7 @@ sub acn_sms_msg {
     }
 
     my $disable_auth = $U->ou_ancestor_setting_value(
-        $org_id || $U->fetch_org_tree->id,
+        $org_id || $U->get_org_tree->id,
         'sms.disable_authentication_requirement.callnumbers'
     );
 
@@ -1310,6 +1689,194 @@ sub acn_sms_msg {
 
 
 
+__PACKAGE__->register_method(
+    method    => "fixed_field_values_by_rec_type",
+    api_name  => "open-ils.cat.biblio.fixed_field_values.by_rec_type",
+    argc      => 2,
+    signature => {
+        desc   => 'Given a record type (as in cmfpm.rec_type), return fixed fields and their possible values as known to the DB',
+        params => [
+            {desc => 'Record Type', type => 'string'},
+            {desc => '(Optional) Fixed field', type => 'string'},
+        ]
+    },
+    return => {desc => 'an object in which the keys are fixed fields and the values are arrays representing the set of all unique values for that fixed field in that record type', type => 'object' }
+);
+
+
+sub fixed_field_values_by_rec_type {
+    my ($self, $conn, $rec_type, $fixed_field) = @_;
+
+    my $e = new_editor;
+    my $values = $e->json_query({
+        select => {
+            crad  => ["fixed_field"],
+            ccvm  => [qw/code value/],
+            cmfpm => [qw/length default_val/],
+        },
+        distinct => 1,
+        from => {
+            ccvm => {
+                crad => {
+                    join => {
+                        cmfpm => {
+                            fkey => "fixed_field",
+                            field => "fixed_field"
+                        }
+                    }
+                }
+            }
+        },
+        where => {
+            "+cmfpm" => {rec_type => $rec_type},
+            defined $fixed_field ?
+                ("+crad" => {fixed_field => $fixed_field}) : ()
+        },
+        order_by => [
+            {class => "crad", field => "fixed_field"},
+            {class => "ccvm", field => "code"}
+        ]
+    }) or return $e->die_event;
+
+    my $result = {};
+    for my $row (@$values) {
+        $result->{$row->{fixed_field}} ||= [];
+        push @{$result->{$row->{fixed_field}}}, [@$row{qw/code value length default_val/}];
+    }
+
+    return $result;
+}
+
+__PACKAGE__->register_method(
+    method    => "retrieve_tag_table",
+    api_name  => "open-ils.cat.tag_table.all.retrieve.local",
+    stream    => 1,
+    argc      => 3,
+    signature => {
+        desc   => "Retrieve set of MARC tags, subfields, and indicator values for the user's OU",
+        params => [
+            {desc => 'Authtoken', type => 'string'},
+            {desc => 'MARC Format', type => 'string'},
+            {desc => 'MARC Record Type', type => 'string'},
+        ]
+    },
+    return => {desc => 'Structure representing the tag table available to that user', type => 'object' }
+);
+__PACKAGE__->register_method(
+    method    => "retrieve_tag_table",
+    api_name  => "open-ils.cat.tag_table.all.retrieve.stock",
+    stream    => 1,
+    argc      => 3,
+    signature => {
+        desc   => 'Retrieve set of MARC tags, subfields, and indicator values for stock MARC standard',
+        params => [
+            {desc => 'Authtoken', type => 'string'},
+            {desc => 'MARC Format', type => 'string'},
+            {desc => 'MARC Record Type', type => 'string'},
+        ]
+    },
+    return => {desc => 'Structure representing the stock tag table', type => 'object' }
+);
+__PACKAGE__->register_method(
+    method    => "retrieve_tag_table",
+    api_name  => "open-ils.cat.tag_table.field_list.retrieve.local",
+    stream    => 1,
+    argc      => 3,
+    signature => {
+        desc   => "Retrieve set of MARC tags for available to the user's OU",
+        params => [
+            {desc => 'Authtoken', type => 'string'},
+            {desc => 'MARC Format', type => 'string'},
+            {desc => 'MARC Record Type', type => 'string'},
+        ]
+    },
+    return => {desc => 'Structure representing the tags available to that user', type => 'object' }
+);
+__PACKAGE__->register_method(
+    method    => "retrieve_tag_table",
+    api_name  => "open-ils.cat.tag_table.field_list.retrieve.stock",
+    stream    => 1,
+    argc      => 3,
+    signature => {
+        desc   => 'Retrieve set of MARC tags for stock MARC standard',
+        params => [
+            {desc => 'Authtoken', type => 'string'},
+            {desc => 'MARC Format', type => 'string'},
+            {desc => 'MARC Record Type', type => 'string'},
+        ]
+    },
+    return => {desc => 'Structure representing the stock MARC tags', type => 'object' }
+);
+
+sub retrieve_tag_table {
+    my( $self, $conn, $auth, $marc_format, $marc_record_type ) = @_;
+    my $e = new_editor( authtoken=>$auth, xact=>1 );
+    return $e->die_event unless $e->checkauth;
+
+    my $field_list_only = ($self->api_name =~ /\.field_list\./) ? 1 : 0;
+    my $context_ou;
+    if ($self->api_name =~ /\.local$/) {
+        $context_ou = $e->requestor->ws_ou;
+    }
+
+    my %sf_by_tag;
+    unless ($field_list_only) {
+        my $subfields = $e->json_query(
+            { from => [ 'config.ou_marc_subfields', 1, $marc_record_type, $context_ou ] }
+        );
+        foreach my $sf (@$subfields) {
+            my $sf_data = {
+                code        => $sf->{code},
+                description => $sf->{description},
+                mandatory   => $sf->{mandatory},
+                repeatable   => $sf->{repeatable},
+            };
+            if ($sf->{value_ctype}) {
+                $sf_data->{value_list} = $e->json_query({
+                    select => { ccvm => [
+                                            'code',
+                                            { column => 'value', alias => 'description' }
+                                        ]
+                              },
+                    from   => 'ccvm',
+                    where  => { ctype => $sf->{value_ctype} },
+                    order_by => { ccvm => { code => {} } },
+                });
+            }
+            push @{ $sf_by_tag{$sf->{tag}} }, $sf_data;
+        }
+    }
+
+    my $fields = $e->json_query(
+        { from => [ 'config.ou_marc_fields', 1, $marc_record_type, $context_ou ] }
+    );
+
+    foreach my $field (@$fields) {
+        next if $field->{hidden} eq 't';
+        unless ($field_list_only) {
+            my $tag = $field->{tag};
+            if ($tag ge '010') {
+                for my $pos (1..2) {
+                    my $ind_ccvm_key = "${marc_format}_${marc_record_type}_${tag}_ind_${pos}";
+                    my $indvals = $e->json_query({
+                        select => { ccvm => [
+                                                'code',
+                                                { column => 'value', alias => 'description' }
+                                            ]
+                                  },
+                        from   => 'ccvm',
+                        where  => { ctype => $ind_ccvm_key }
+                    });
+                    next unless defined($indvals);
+                    $field->{"ind$pos"} = $indvals;
+                }
+                $field->{subfields} = exists($sf_by_tag{$tag}) ? $sf_by_tag{$tag} : [];
+            }
+        }
+        $conn->respond($field);
+    }
+}
+
 1;
 
 # vi:et:ts=4:sw=4