From d8aedef03e5315a8e91fc99f9e18380f85c4e439 Mon Sep 17 00:00:00 2001 From: Zeno Tajoli Date: Thu, 19 Oct 2023 22:53:43 +0200 Subject: [PATCH] Bug 33036: REST API: Merge biblio records implements merging of records + attached items, subscriptions etc via the API as an alternative to the web interface: cgi-bin/koha/cataloguing/merge.pl To test: 1) you need an API user with the permissions "editcatalogue" 2) two records: one to be merged into (with biblio_id, eg 262) and another one from which to merge (with biblio_id_to_merge, eg 9) which will be deleted! both records may/should have items, subscription, subscriptionhistory, serial, suggestions orders and holds 3) check both records via the web 4) Apply patch 5) execute API calls eg like PUT /biblios/{biblio_id}/merge/{biblio_id_to_merge} eg: curl -s -u koha:koha -X POST "http://127.0.0.1:8081/api/v1/biblios/262/merge/9" 6) the record with the id is deleted now, the record with has all items, etc attached, return code is 200, with the message {"message":"Successfuly merged 9 into 262"} 7) optionally a full MARCXML record may be sent as body of the API call curl -s -u koha:koha -X POST "http://127.0.0.1:8081/api/v1/biblios/262/merge/2" -d @marcfile.xml 8) now also the content of the record with is replaced with the content of the MARCXML file 9) Go into intramet and do a search. Select two or (better) more record. 10) Merge them; merge must be a success. 11) Test with prove -v t/db_dependent/Koha/Biblio.t To do test with curl you canuse the MARCXML (MARC21) file attached in bugzilla. Sponsored-by: Technische Hochschule Wildau Co-authored-by: domm@plix.at Signed-off-by: Jan Kissig --- Koha/Biblio.pm | 88 +++++++++++++++++++++++++ Koha/REST/V1/Biblios.pm | 60 +++++++++++++++++ api/v1/swagger/paths/biblios_merge.yaml | 47 +++++++++++++ api/v1/swagger/swagger.yaml | 2 + cataloguing/merge.pl | 63 ++++-------------- t/db_dependent/Koha/Biblio.t | 31 ++++++++- 6 files changed, 237 insertions(+), 54 deletions(-) create mode 100644 api/v1/swagger/paths/biblios_merge.yaml diff --git a/Koha/Biblio.pm b/Koha/Biblio.pm index 9e2386ae98..7e68a58ad3 100644 --- a/Koha/Biblio.pm +++ b/Koha/Biblio.pm @@ -24,6 +24,11 @@ use URI; use URI::Escape qw( uri_escape_utf8 ); use C4::Koha qw( GetNormalizedISBN GetNormalizedUPC GetNormalizedOCLCNumber ); +use C4::Biblio qw( DelBiblio AddBiblio ModBiblio ); +use C4::Serials qw( CountSubscriptionFromBiblionumber ); +use C4::Serials qw( CountSubscriptionFromBiblionumber ); +use C4::Reserves qw( MergeHolds ); +use C4::Acquisition qw( ModOrder GetOrdersByBiblionumber ); use Koha::Database; use Koha::DateUtils qw( dt_from_string ); @@ -1746,6 +1751,89 @@ sub opac_summary_html { return $summary_html; } +=head3 merge_with + + my $biblio = Koha::Biblios->find($biblionumber); + $biblio->merge_with(\@biblio_ids); + + This subroutine merges a list of bibliographic records into the bibliographic record. + This function DOES NOT CHANGE the bibliographic metadata of the record. But it links all + items, holds, subscriptions, serials issues and article_requests to the record. After doing changes + bibliographic records listed are deleted + +=cut + +sub merge_with { + my ( $self, $biblio_ids ) = @_; + my @biblio_ids_to_merge = @{$biblio_ids}; + my $dbh = C4::Context->dbh; + my $ref_biblionumber = $self->biblionumber; + # Ensure the keeper isn't in the list of records to merge + @biblio_ids_to_merge = grep { $_ ne $ref_biblionumber } @biblio_ids_to_merge; + my @worked_ids = (); + my %results = ( 'biblio_id' => $ref_biblionumber, 'merged_ids' => \@worked_ids, 'error' => '', 'status' => 1 ); + #my $biblio = Koha::Biblios->find($self->biblionumber); + + foreach (@biblio_ids_to_merge) { + my $bn_merge = $_; + eval { + my $from_biblio = Koha::Biblios->find($bn_merge); + $from_biblio->items->move_to_biblio($self); + $from_biblio->article_requests->update( { biblionumber => $ref_biblionumber }, { no_triggers => 1 } ); + + # Moving subscriptions from the other record to the reference record + my $sth_subscription = $dbh->prepare( " + UPDATE subscription SET biblionumber = ? WHERE biblionumber = ? + " ); + my $sth_subscriptionhistory = $dbh->prepare( " + UPDATE subscriptionhistory SET biblionumber = ? WHERE biblionumber = ? + " ); + my $subcount = CountSubscriptionFromBiblionumber($bn_merge); + if ( $subcount > 0 ) { + $sth_subscription->execute( $ref_biblionumber, $bn_merge ); + $sth_subscriptionhistory->execute( $ref_biblionumber, $bn_merge ); + } + + # Moving serials + my $sth_serial = $dbh->prepare( " + UPDATE serial SET biblionumber = ? WHERE biblionumber = ? + " ); + $sth_serial->execute( $ref_biblionumber, $bn_merge ); + + # Moving suggestions + my $sth_suggestions = $dbh->prepare( " + UPDATE suggestions SET biblionumber = ? WHERE biblionumber = ? + " ); + $sth_suggestions->execute( $ref_biblionumber, $bn_merge ); + # Moving orders (orders linked to items of frombiblio have already been moved by move_to_biblio) + my @allorders = GetOrdersByBiblionumber($bn_merge); + foreach my $myorder (@allorders) { + $myorder->{'biblionumber'} = $ref_biblionumber; + ModOrder($myorder); + # TODO : add error control (in ModOrder?) + } + + # Move holds + MergeHolds( $dbh, $ref_biblionumber, $bn_merge ); + }; + if ($@) { + $results{'error'} = $@; + $results{'status'} = 0; + return \%results; + } + # Deleting record + my $error = DelBiblio($bn_merge); + if ($error) { + $results{'error'} = $error; + $results{'status'} = 0; + return \%results; + } else { + push( @worked_ids, $bn_merge ); + } + } + return \%results; +} + =head2 Internal methods =head3 type diff --git a/Koha/REST/V1/Biblios.pm b/Koha/REST/V1/Biblios.pm index eef084bc2c..a6f10a0714 100644 --- a/Koha/REST/V1/Biblios.pm +++ b/Koha/REST/V1/Biblios.pm @@ -860,4 +860,64 @@ sub list { }; } +=head3 merge + +Controller function that handles merging two biblios. If an optional +MARCXML is provided as the request body, this MARCXML replaces the +bibliodata of the merge target biblio. Syntax format inside the request body +must match with the Marc format used into Koha installation (MARC21 or UNIMARC) + +=cut + +sub merge { + my $c = shift->openapi->valid_input or return; + my $ref_biblionumber = $c->param('biblio_id'); + my $bn_merge = $c->param('biblio_id_to_merge'); + my $dbh = C4::Context->dbh; + + my $body = $c->req->body; + my $biblio = Koha::Biblios->find($ref_biblionumber); + if ( not defined $biblio ) { + return $c->render( + status => 404, + json => { error => sprintf( "[%s] biblio to merge into not found", $ref_biblionumber ) } + ); + } + + my $from_biblio = Koha::Biblios->find($bn_merge); + if ( not defined $from_biblio ) { + return $c->render( + status => 404, + # openapi => { error => sprintf("[%s] biblio to merge from not found", $bn_merge) } + json => { error => sprintf( "[%s] biblio to merge from not found", $bn_merge ) } + ); + } + my $results; + eval { + if ($body) { + my $record = MARC::Record->new_from_xml( $body, 'UTF-8', C4::Context->preference("marcflavour") ); + $record->leader( $biblio->metadata->record->leader() ); + ModBiblio( $record, $ref_biblionumber, $biblio->frameworkcode ); + } + my @biblio_ids_to_merge = ($bn_merge); + $results = $biblio->merge_with( \@biblio_ids_to_merge ); + }; + if ($@) { + return $c->render( status => 400, json => { error => $@ } ); + } else { + if ( $results->{'status'} == 1 ) { + return $c->render( + status => 200, + openapi => { + message => sprintf( "Successfully merged %s into %s", $bn_merge, $ref_biblionumber ), + merged => $bn_merge, + kept => $ref_biblionumber, + } + ); + } else { + return $c->render( status => 400, json => { error => $results->{'error'} } ); + } + } +} + 1; diff --git a/api/v1/swagger/paths/biblios_merge.yaml b/api/v1/swagger/paths/biblios_merge.yaml new file mode 100644 index 0000000000..caafbd4022 --- /dev/null +++ b/api/v1/swagger/paths/biblios_merge.yaml @@ -0,0 +1,47 @@ +"/biblios/{biblio_id}/merge/{biblio_id_to_merge}": + post: + x-mojo-to: Biblios#merge + operationId: mergeBiblios + tags: + - merge_biblios + summary: Merge Biblios + parameters: + - name: biblio_id + in: path + description: Bilblionumber to be merged into + required: true + type: string + - name: biblio_id_to_merge + in: path + required: true + description: Biblionumber from which to merge + type: string + - $ref: "../swagger.yaml#/parameters/biblio_id_pp" + - $ref: "../swagger.yaml#/parameters/framework_id_header" + - $ref: "../swagger.yaml#/parameters/marc_schema_header" + responses: + '200': + description: Result message + '404': + description: Biblio not found + schema: + "$ref": "../swagger.yaml#/definitions/error" + '401': + description: Authentication required + schema: + "$ref": "../swagger.yaml#/definitions/error" + '403': + description: Access forbidden + schema: + "$ref": "../swagger.yaml#/definitions/error" + '500': + description: Internal server error + schema: + "$ref": "../swagger.yaml#/definitions/error" + '503': + description: Under maintenance + schema: + "$ref": "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + catalogue: "editcatalogue" diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index 351f796c56..58ff9403f2 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -207,6 +207,8 @@ paths: $ref: "./paths/biblios_item_groups.yaml#/~1biblios~1{biblio_id}~1item_groups~1{item_group_id}~1items" "/biblios/{biblio_id}/item_groups/{item_group_id}/items/{item_id}": $ref: "./paths/biblios_item_groups.yaml#/~1biblios~1{biblio_id}~1item_groups~1{item_group_id}~1items~1{item_id}" + "/biblios/{biblio_id}/merge/{biblio_id_to_merge}": + $ref: "./paths/biblios_merge.yaml#/~1biblios~1{biblio_id}~1merge~1{biblio_id_to_merge}" "/cash_registers/{cash_register_id}/cashups": $ref: "./paths/cash_registers.yaml#/~1cash_registers~1{cash_register_id}~1cashups" "/cashups/{cashup_id}": diff --git a/cataloguing/merge.pl b/cataloguing/merge.pl index 7fe218845e..5899f1b361 100755 --- a/cataloguing/merge.pl +++ b/cataloguing/merge.pl @@ -86,33 +86,11 @@ if ($merge) { # Rewriting the leader my $biblio = Koha::Biblios->find($ref_biblionumber); $record->leader($biblio->metadata->record->leader()); - + #Take new framework code my $frameworkcode = $input->param('frameworkcode'); - # Modifying the reference record ModBiblio($record, $ref_biblionumber, $frameworkcode); - # Moving items and article requests from the other record to the reference record - $biblio = $biblio->get_from_storage; - foreach my $biblionumber (@biblionumbers) { - my $from_biblio = Koha::Biblios->find($biblionumber); - $from_biblio->items->move_to_biblio($biblio); - $from_biblio->article_requests->update({ biblionumber => $ref_biblionumber }, { no_triggers => 1 }); - } - - my $sth_subscription = $dbh->prepare(" - UPDATE subscription SET biblionumber = ? WHERE biblionumber = ? - "); - my $sth_subscriptionhistory = $dbh->prepare(" - UPDATE subscriptionhistory SET biblionumber = ? WHERE biblionumber = ? - "); - my $sth_serial = $dbh->prepare(" - UPDATE serial SET biblionumber = ? WHERE biblionumber = ? - "); - my $sth_suggestions = $dbh->prepare(" - UPDATE suggestions SET biblionumber = ? WHERE biblionumber = ? - "); - my $report_header = {}; foreach my $biblionumber ($ref_biblionumber, @biblionumbers) { # build report @@ -148,37 +126,18 @@ if ($merge) { push @report_records, \%report_record; } - foreach my $biblionumber (@biblionumbers) { - # Moving subscriptions from the other record to the reference record - my $subcount = CountSubscriptionFromBiblionumber($biblionumber); - if ($subcount > 0) { - $sth_subscription->execute($ref_biblionumber, $biblionumber); - $sth_subscriptionhistory->execute($ref_biblionumber, $biblionumber); + my $rmerge; + eval { + my $newbiblio = Koha::Biblios->find($ref_biblionumber); + $rmerge = $newbiblio->merge_with(\@biblionumbers); + }; + if($@) + { push @errors, $@; + } else + { if ($rmerge->{'status'}==0) + { push @errors, $rmerge->{'error'}; } - - # Moving serials - $sth_serial->execute($ref_biblionumber, $biblionumber); - - # Moving suggestions - $sth_suggestions->execute($ref_biblionumber, $biblionumber); - - # Moving orders (orders linked to items of frombiblio have already been moved by move_to_biblio) - my @allorders = GetOrdersByBiblionumber($biblionumber); - foreach my $myorder (@allorders) { - $myorder->{'biblionumber'} = $ref_biblionumber; - ModOrder ($myorder); - # TODO : add error control (in ModOrder?) - } - - # Deleting the other records - if (scalar(@errors) == 0) { - # Move holds - MergeHolds($dbh, $ref_biblionumber, $biblionumber); - my $error = DelBiblio($biblionumber); - push @errors, $error if ($error); } -} - # Parameters $template->param( result => 1, diff --git a/t/db_dependent/Koha/Biblio.t b/t/db_dependent/Koha/Biblio.t index 13be09f466..bbe8d3639b 100755 --- a/t/db_dependent/Koha/Biblio.t +++ b/t/db_dependent/Koha/Biblio.t @@ -17,7 +17,7 @@ use Modern::Perl; -use Test::More tests => 30; +use Test::More tests => 31; use Test::Exception; use Test::Warn; @@ -486,13 +486,40 @@ subtest 'to_api() tests' => sub { $schema->storage->txn_rollback; }; +subtest 'merge of records' => sub { + plan tests => 1; + $schema->storage->txn_begin; + + #Three biblio + my $biblio1 = $builder->build_sample_biblio( { title => 'Title number 1' } ); + my $biblio2 = $builder->build_sample_biblio( { title => 'Title number 2' } ); + my $biblio3 = $builder->build_sample_biblio( { title => 'Title number 3' } ); + + # One items each + my $item1_1 = $builder->build_sample_item( { biblionumber => $biblio1->biblionumber, barcode => 'bar11' } ); + my $item2_1 = $builder->build_sample_item( { biblionumber => $biblio2->biblionumber, barcode => 'bar22' } ); + my $item3_1 = $builder->build_sample_item( { biblionumber => $biblio3->biblionumber, barcode => 'bar33' } ); + my $results = ''; + my @to_merge = ( $biblio2->biblionumber, $biblio3->biblionumber ); + eval { $results = $biblio1->merge_with( \@to_merge ); }; + if($@) { + is( 0, 1, "Not working. Error: " . $@ ); + } else { + my $items = $biblio1->items; + is( $items->count, 3, "After merge we have 3 items on first record" ); + } + #End work + #say Dumper $results; + $schema->storage->txn_rollback; +}; + subtest 'suggestions() tests' => sub { plan tests => 3; $schema->storage->txn_begin; - my $biblio = $builder->build_sample_biblio(); + my $biblio = $builder->build_sample_biblio(); is( ref($biblio->suggestions), 'Koha::Suggestions', 'Return type is correct' ); -- 2.34.1