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 91566c2..3fcd255 100644 (file)
@@ -263,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;
 
     $conn->respond_complete(
-        $actor->request('open-ils.actor.anon_cache.set_value', $auth, res_list => $responses)->gather(1)
+        $actor->request('open-ils.actor.anon_cache.set_value', $auth, batch_edit_progress => {})->gather(1)
     ) if ($actor);
 
     for my $item ( @$items ) {
@@ -281,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 });
         }
@@ -294,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' };
@@ -304,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' };
         }
@@ -498,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));
@@ -506,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);
         }
     }
@@ -640,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 );
         }
@@ -660,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,
@@ -680,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(
@@ -799,10 +852,16 @@ sub transfer_copies_to_volume {
 
     # flesh and munge the copies
     my $fleshed_copies = [];
-    my ($copy, $copy_evt);
+    my $copy;
     foreach my $copy_id ( @{ $copies } ) {
-        ($copy, $copy_evt) = $U->fetch_copy($copy_id);
-        return $copy_evt if $copy_evt;
+        $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' );
@@ -820,6 +879,36 @@ sub transfer_copies_to_volume {
         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);
@@ -914,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",);
 
@@ -936,6 +1141,7 @@ sub fleshed_volume_update {
     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);
@@ -965,14 +1171,55 @@ sub fleshed_volume_update {
 
         } elsif( $vol->isnew ) {
             $logger->info("vol-update: creating volume");
-            $evt = $assetcom->create_volume( $oargs, $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, ($oargs->{all} or grep { $_ eq 'VOLUME_LABEL_EXISTS' } @{$oargs->{events}} 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
@@ -981,12 +1228,17 @@ sub fleshed_volume_update {
             $evt = $assetcom->update_fleshed_copies(
                 $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);
+    }
 }
 
 
@@ -1127,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(
@@ -1178,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) {
@@ -1190,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) {
@@ -1517,7 +1812,6 @@ 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;
-    return $e->die_event unless $e->allowed('UPDATE_MARC', $e->requestor->ws_ou);
 
     my $field_list_only = ($self->api_name =~ /\.field_list\./) ? 1 : 0;
     my $context_ou;