From 7af5a85d00059815cf4c547dc4deada65fbd15a9 Mon Sep 17 00:00:00 2001
From: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Date: Thu, 6 Apr 2023 17:00:21 +0200
Subject: [PATCH] Bug 30708: Duplicate/copy items to an opened train

Sponsored-by: BULAC - http://www.bulac.fr/
Signed-off-by: BULAC - http://www.bulac.fr/
Signed-off-by: Heather Hernandez <heather_hernandez@nps.gov>
Signed-off-by: Laurence Rault <laurence.rault@biblibre.com>
---
 Koha/Preservation/Train.pm                    | 21 +++--
 Koha/REST/V1/Preservation/Trains.pm           | 92 +++++++++++++++++++
 api/v1/swagger/paths/preservation_trains.yaml | 75 +++++++++++++++
 api/v1/swagger/swagger.yaml                   |  2 +
 .../components/Preservation/TrainsShow.vue    | 38 +++-----
 .../js/vue/fetch/preservation-api-client.js   |  5 +
 t/db_dependent/Koha/Preservation/Trains.t     |  8 +-
 7 files changed, 206 insertions(+), 35 deletions(-)

diff --git a/Koha/Preservation/Train.pm b/Koha/Preservation/Train.pm
index dadb1dd13d7..b87f0eb8788 100644
--- a/Koha/Preservation/Train.pm
+++ b/Koha/Preservation/Train.pm
@@ -56,35 +56,42 @@ sub default_processing {
 
 Add item to this train
 
-my $train_item = $train->add_item({item_id => $itemnumber, processing_id => $processing_id});
-my $train_item = $train->add_item({barcode => $barcode, processing_id => $processing_id});
+my $train_item = $train->add_item({item_id => $itemnumber, processing_id => $processing_id}, { skip_waiting_list_check => 0|1 });
+my $train_item = $train->add_item({barcode => $barcode, processing_id => $processing_id, { skip_waiting_list_check => 0|1 });
+
+skip_waitin_list_check can be set to true if the item can be added to the train even if the item is not in the waiting list.
 
 =cut
 
 sub add_item {
-    my ( $self, $train_item ) = @_;
+    my ( $self, $train_item, $params ) = @_;
+
+    my $skip_waiting_list_check = $params->{skip_waiting_list_check} || 0;
 
     my $not_for_loan = C4::Context->preference('PreservationNotForLoanWaitingListIn');
 
     my $key  = exists $train_item->{item_id} ? 'itemnumber' : 'barcode';
     my $item = Koha::Items->find( { $key => $train_item->{item_id} || $train_item->{barcode} } );
     Koha::Exceptions::Preservation::ItemNotFound->throw unless $item;
-    Koha::Exceptions::Preservation::ItemNotInWaitingList->throw if $item->notforloan != $not_for_loan;
+
+    Koha::Exceptions::Preservation::ItemNotInWaitingList->throw
+      if !$skip_waiting_list_check && $item->notforloan != $not_for_loan;
 
     # FIXME We need a LOCK here
     # Not important for now as we have add_items
     # Note that there are several other places in Koha with this max+1 problem
     my $max = $self->items->search->_resultset->get_column("user_train_item_id")->max || 0;
-    my $train_item_rs = $self->_result->add_to_preservation_trains_items(
+    my $train_item_object = Koha::Preservation::Train::Item->new(
         {
+            train_id      => $self->train_id,
             item_id       => $item->itemnumber,
             processing_id => $train_item->{processing_id} || $self->default_processing_id,
             user_train_item_id => $max + 1,
             added_on      => \'NOW()',
         }
-    );
+    )->store;
     $item->notforloan( $self->not_for_loan )->store;
-    return Koha::Preservation::Train::Item->_new_from_dbic($train_item_rs);
+    return $train_item_object->get_from_storage;
 }
 
 =head3 add_items
diff --git a/Koha/REST/V1/Preservation/Trains.pm b/Koha/REST/V1/Preservation/Trains.pm
index 644a7a97215..cb70d2f74bd 100644
--- a/Koha/REST/V1/Preservation/Trains.pm
+++ b/Koha/REST/V1/Preservation/Trains.pm
@@ -389,6 +389,98 @@ sub add_item {
     };
 }
 
+=head3 copy_item
+
+=cut
+
+sub copy_item {
+    my $c = shift->openapi->valid_input or return;
+
+    my $train_id = $c->validation->param('train_id');
+    my $train = Koha::Preservation::Trains->find( $train_id );
+
+    unless ($train) {
+        return $c->render(
+            status  => 404,
+            openapi => { error => "Train not found" }
+        );
+    }
+
+    my $train_item_id = $c->validation->param('train_item_id');
+
+    my $train_item = Koha::Preservation::Train::Items->search({ train_item_id => $train_item_id, train_id => $train_id })->single;
+
+    unless ($train_item) {
+        return $c->render(
+            status  => 404,
+            openapi => { error => "Item not found" }
+        );
+    }
+
+    my $body = $c->validation->param('body');
+    return try {
+        Koha::Database->new->schema->txn_do(
+            sub {
+                my $new_train_id = delete $body->{train_id};
+                my $new_train = Koha::Preservation::Trains->find( $new_train_id );
+                unless ($train) {
+                    return $c->render(
+                        status  => 404,
+                        openapi => { error => "Train not found" }
+                    );
+                }
+                my $new_train_item = $new_train->add_item(
+                    {
+                        item_id       => $train_item->item_id,
+                        processing_id => $train_item->processing_id
+                    },
+                    {
+                        skip_waiting_list_check => 1,
+                    },
+                );
+                my $attributes = [
+                    map {
+                        {
+                            processing_attribute_id => $_->processing_attribute_id,
+                            train_item_id => $new_train_item->train_item_id,
+                            value         => $_->value
+                        }
+                    } $train_item->attributes->as_list
+                  ];
+                $new_train_item->attributes($attributes);
+                return $c->render( status => 201, openapi => $train_item );
+              }
+        );
+    }
+    catch {
+        if ( blessed $_ ) {
+            if ( $_->isa('Koha::Exceptions::Preservation::MissingSettings') ) {
+                return $c->render(
+                    status  => 400,
+                    openapi => { error => "MissingSettings", parameter => $_->parameter }
+                );
+            } elsif ( $_->isa('Koha::Exceptions::Preservation::ItemNotFound') ) {
+                return $c->render(
+                    status  => 404,
+                    openapi => { error => "Item not found" }
+                );
+            } elsif ( $_->isa('Koha::Exceptions::Object::DuplicateID') ) {
+                return $c->render(
+                    status  => 409,
+                    openapi => { error => $_->error, conflict => $_->duplicate_id }
+                );
+            } elsif ( $_->isa('Koha::Exceptions::Preservation::ItemNotInWaitingList') ) {
+                return $c->render(
+                    status  => 400,
+                    openapi => { error => 'Item not in waiting list' }
+                );
+            }
+        }
+
+        $c->unhandled_exception($_);
+    };
+}
+
 =head3 update_item
 
 Controller function that handles updating an item from a train
diff --git a/api/v1/swagger/paths/preservation_trains.yaml b/api/v1/swagger/paths/preservation_trains.yaml
index e7fc5c90f83..dd4058e475a 100644
--- a/api/v1/swagger/paths/preservation_trains.yaml
+++ b/api/v1/swagger/paths/preservation_trains.yaml
@@ -619,3 +619,78 @@
     x-koha-authorization:
       permissions:
         preservation: 1
+
+"/preservation/trains/{train_id}/items/{train_item_id}/copy":
+  post:
+    x-mojo-to: Preservation::Trains#copy_item
+    operationId: copyItemToAnotherTrain
+    tags:
+      - preservation_train
+    summary: Copy an item to an other train
+    consumes:
+      - application/json
+    produces:
+      - application/json
+    parameters:
+      - $ref: "../swagger.yaml#/parameters/preservation_train_id_pp"
+      - $ref: "../swagger.yaml#/parameters/preservation_train_item_id_pp"
+      - description: The train_id of the new train
+        in: body
+        name: body
+        required: true
+        schema:
+          type: object
+    responses:
+      201:
+        description: A successfully copied item
+        schema:
+          type: object
+      400:
+        description: Bad parameter
+        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"
+      404:
+        description: Ressource not found
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      409:
+        description: Conflict in creating resource
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      413:
+        description: Payload too large
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      500:
+        description: |-
+          Internal server error. Possible `error_code` attribute values:
+          * `internal_server_error`
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      503:
+        description: Under maintenance
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+    x-koha-authorization:
+      permissions:
+        preservation: 1
+        description: |-
+          Internal server error. Possible `error_code` attribute values:
+          * `internal_server_error`
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      503:
+        description: Under maintenance
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+    x-koha-authorization:
+      permissions:
+        preservation: 1
diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml
index 4437f4e2d57..cc18bdf095b 100644
--- a/api/v1/swagger/swagger.yaml
+++ b/api/v1/swagger/swagger.yaml
@@ -331,6 +331,8 @@ paths:
     $ref: "./paths/preservation_trains.yaml#/~1preservation~1trains~1{train_id}~1items~1batch"
   "/preservation/trains/{train_id}/items/{train_item_id}":
     $ref: "./paths/preservation_trains.yaml#/~1preservation~1trains~1{train_id}~1items~1{train_item_id}"
+  "/preservation/trains/{train_id}/items/{train_item_id}/copy":
+    $ref: "./paths/preservation_trains.yaml#/~1preservation~1trains~1{train_id}~1items~1{train_item_id}~1copy"
   /preservation/processings:
     $ref: ./paths/preservation_processings.yaml#/~1preservation~1processings
   "/preservation/processings/{processing_id}":
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsShow.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsShow.vue
index 978b08986c3..6d522f88da3 100644
--- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsShow.vue
+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Preservation/TrainsShow.vue
@@ -410,34 +410,18 @@ export default {
         copyItem(event) {
             event.preventDefault()
             const client = APIClient.preservation
-            let new_train_item = {}
             client.train_items
-                .get(this.train.train_id, this.train_item_id_to_copy)
-                .then(train_item => {
-                    new_train_item = {
-                        attributes: train_item.attributes.map(attr => {
-                            return {
-                                processing_attribute_id:
-                                    attr.processing_attribute_id,
-                                value: attr.value,
-                            }
-                        }),
-                        item_id: train_item.item_id,
-                        processing_id: train_item.processing_id,
-                    }
-                })
-                .then(() =>
-                    client.train_items
-                        .create(new_train_item, this.train_id_selected_for_copy)
-                        .then(
-                            success => {
-                                this.setMessage(
-                                    this.$__("Item copied successfully.")
-                                )
-                                this.show_modal = false
-                            },
-                            error => {}
-                        )
+                .copy(
+                    this.train_id_selected_for_copy,
+                    this.train.train_id,
+                    this.train_item_id_to_copy
+                )
+                .then(
+                    success => {
+                        this.setMessage(this.$__("Item copied successfully."))
+                        this.show_modal = false
+                    },
+                    error => {}
                 )
         },
         build_datatable: function () {
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/preservation-api-client.js b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/preservation-api-client.js
index 455a80be5e7..356adfac4b6 100644
--- a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/preservation-api-client.js
+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/preservation-api-client.js
@@ -121,6 +121,11 @@ export class PreservationAPIClient extends HttpClient {
                     endpoint: "trains/" + train_id + "/items/batch",
                     body: train_items,
                 }),
+            copy: (new_train_id, train_id, id) =>
+                this.post({
+                    endpoint: "trains/" + train_id + "/items/" + id + "/copy",
+                    body: { train_id: new_train_id },
+                }),
             update: (train_item, train_id, id) =>
                 this.put({
                     endpoint: "trains/" + train_id + "/items/" + id,
diff --git a/t/db_dependent/Koha/Preservation/Trains.t b/t/db_dependent/Koha/Preservation/Trains.t
index 9fd1ea2c3ce..7bf7398ae4e 100755
--- a/t/db_dependent/Koha/Preservation/Trains.t
+++ b/t/db_dependent/Koha/Preservation/Trains.t
@@ -52,7 +52,7 @@ subtest 'default_processing' => sub {
 };
 
 subtest 'add_items & items' => sub {
-    plan tests => 9;
+    plan tests => 12;
 
     $schema->storage->txn_begin;
 
@@ -98,5 +98,11 @@ subtest 'add_items & items' => sub {
     is( $item_2->get_from_storage->notforloan, 0 );
     is( $item_3->get_from_storage->notforloan, $not_for_loan_train_in );
 
+    warning_is {
+        $train->add_item( { item_id => $item_2->itemnumber }, { skip_waiting_list_check => 1 } );
+    } '';
+    is( $train->items->count, 3, 'the item has been added to the train' );
+    is( $item_2->get_from_storage->notforloan, $not_for_loan_train_in );
+
     $schema->storage->txn_rollback;
 };
-- 
2.25.1