Bug 32121: Show an alert when adding a checked out item to an item bundle
authorJulian Maurice <julian.maurice@biblibre.com>
Mon, 7 Nov 2022 13:30:34 +0000 (14:30 +0100)
committerTomas Cohen Arazi <tomascohen@theke.io>
Mon, 27 Mar 2023 10:50:02 +0000 (12:50 +0200)
When trying to add a checked out item to an item bundle, an alert
message will show up, giving the user a chance to return the item
immediately before adding it to the bundle

Test plan:
1. Create an item bundle (see bug 28854 comment 458)
2. Create a biblio with one item and check out this item.
3. Try to add the checked out item to the bundle
   You should see a message saying that the item is checked out. Next to
   this message should be a button "Check in and add to bundle".
4. Click on the button. There should be a message saying that the item
   was added to the bundle.
5. Close the modal window and verify that the item was correctly
   returned and added to the bundle

Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
Koha/Exceptions/Checkin.pm [new file with mode: 0644]
Koha/Exceptions/Item/Bundle.pm
Koha/Item.pm
Koha/REST/V1/Items.pm
api/v1/swagger/definitions/bundle_link.yaml
koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt
t/db_dependent/Koha/Item.t

diff --git a/Koha/Exceptions/Checkin.pm b/Koha/Exceptions/Checkin.pm
new file mode 100644 (file)
index 0000000..cdc1533
--- /dev/null
@@ -0,0 +1,32 @@
+package Koha::Exceptions::Checkin;
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+use Koha::Exception;
+
+use Exception::Class (
+    'Koha::Exceptions::Checkin' => {
+        isa => 'Koha::Exception',
+    },
+    'Koha::Exceptions::Checkin::FailedCheckin' => {
+        isa         => 'Koha::Exceptions::Checkin',
+        description => "Checkin failed"
+    },
+);
+
+1;
index 0ad0a2a..8c42fd2 100644 (file)
@@ -20,14 +20,17 @@ use Modern::Perl;
 use Koha::Exception;
 
 use Exception::Class (
-
     'Koha::Exceptions::Item::Bundle' => {
         isa => 'Koha::Exception',
     },
     'Koha::Exceptions::Item::Bundle::IsBundle' => {
         isa         => 'Koha::Exceptions::Item::Bundle',
         description => "A bundle cannot be added to a bundle",
-    }
+    },
+    'Koha::Exceptions::Item::Bundle::ItemIsCheckedOut' => {
+        isa         => 'Koha::Exceptions::Item::Bundle',
+        description => 'Someone tried to add a checked out item to a bundle',
+    },
 );
 
 =head1 NAME
@@ -44,6 +47,10 @@ Generic Item::Bundle exception
 
 Exception to be used when attempting to add one bundle into another.
 
+=head2 Koha::Exceptions::Item::Bundle::ItemIsCheckedOut
+
+Exception to be used when attempting to add a checked out item to a bundle.
+
 =cut
 
 1;
index 3fd37cc..7e18908 100644 (file)
@@ -36,6 +36,8 @@ use Koha::Biblio::ItemGroups;
 use Koha::Checkouts;
 use Koha::CirculationRules;
 use Koha::CoverImages;
+use Koha::Exceptions::Checkin;
+use Koha::Exceptions::Item::Bundle;
 use Koha::Exceptions::Item::Transfer;
 use Koha::Item::Attributes;
 use Koha::Exceptions::Item::Bundle;
@@ -1699,7 +1701,9 @@ Adds the bundle_item passed to this item
 =cut
 
 sub add_to_bundle {
-    my ( $self, $bundle_item ) = @_;
+    my ( $self, $bundle_item, $options ) = @_;
+
+    $options //= {};
 
     Koha::Exceptions::Item::Bundle::IsBundle->throw()
       if ( $self->itemnumber eq $bundle_item->itemnumber
@@ -1713,6 +1717,19 @@ sub add_to_bundle {
     try {
         $schema->txn_do(
             sub {
+                my $checkout = $bundle_item->checkout;
+                if ($checkout) {
+                    unless ($options->{force_checkin}) {
+                        Koha::Exceptions::Item::Bundle::ItemIsCheckedOut->throw();
+                    }
+
+                    my $branchcode = C4::Context->userenv->{'branch'};
+                    my ($success) = C4::Circulation::AddReturn($bundle_item->barcode, $branchcode);
+                    unless ($success) {
+                        Koha::Exceptions::Checkin::FailedCheckin->throw();
+                    }
+                }
+
                 $self->_result->add_to_item_bundles_hosts(
                     { item => $bundle_item->itemnumber } );
 
@@ -1764,7 +1781,7 @@ sub add_to_bundle {
             $_->rethrow();
         }
         else {
-            $_;
+            $_->rethrow();
         }
     };
 }
index 013ac03..3f39009 100644 (file)
@@ -278,7 +278,8 @@ sub add_to_bundle {
     }
 
     return try {
-        my $link = $item->add_to_bundle($bundle_item);
+        my $force_checkin = $c->validation->param('body')->{'force_checkin'};
+        my $link = $item->add_to_bundle($bundle_item, { force_checkin => $force_checkin });
         return $c->render(
             status  => 201,
             openapi => $bundle_item
@@ -301,8 +302,23 @@ sub add_to_bundle {
                     error => 'Bundles cannot be nested'
                 }
             );
-        }
-        else {
+        } elsif (ref($_) eq 'Koha::Exceptions::Item::Bundle::ItemIsCheckedOut') {
+            return $c->render(
+                status  => 409,
+                openapi => {
+                    error => 'Item is checked out',
+                    key   => 'checked_out'
+                }
+            );
+        } elsif (ref($_) eq 'Koha::Exceptions::Checkin::FailedCheckin') {
+            return $c->render(
+                status  => 409,
+                openapi => {
+                    error => 'Item cannot be checked in',
+                    key   => 'failed_checkin'
+                }
+            );
+        } else {
             $c->unhandled_exception($_);
         }
     };
index 572be83..382ad67 100644 (file)
@@ -11,4 +11,8 @@ properties:
       - string
       - "null"
     description: Item barcode
+  force_checkin:
+    type:
+      - boolean
+      - "null"
 additionalProperties: false
index 707a5d7..7a34ce0 100644 (file)
@@ -1937,36 +1937,45 @@ Note that permanent location is a code, and location may be an authval.
                 bundle_form_active = item_id;
             });
 
-            $("#addToBundleForm").submit(function(event) {
-
-                  /* stop form from submitting normally */
-                  event.preventDefault();
-
-                  /* get the action attribute from the <form action=""> element */
-                  var $form = $(this),
-                  url = $form.attr('action');
-
+            function addToBundle (url, data) {
                   /* Send the data using post with external_id */
                   var posting = $.post({
                       url: url,
-                      data: JSON.stringify({ external_id: $('#external_id').val()}),
+                      data: JSON.stringify(data),
                       contentType: "application/json; charset=utf-8",
                       dataType: "json"
                   });
 
+                  const barcode = data.external_id;
+
                   /* Report the results */
                   posting.done(function(data) {
-                      var barcode = $('#external_id').val();
                       $('#addResult').replaceWith('<div id="addResult" class="alert alert-success">'+_("Success: Added '%s'").format(barcode)+'</div>');
                       $('#external_id').val('').focus();
                       bundle_changed = 1;
                   });
                   posting.fail(function(data) {
-                      var barcode = $('#external_id').val();
                       if ( data.status === 409 ) {
                           var response = data.responseJSON;
                           if ( response.key === "PRIMARY" ) {
                               $('#addResult').replaceWith('<div id="addResult" class="alert alert-warning">'+_("Warning: Item '%s' already attached").format(barcode)+'</div>');
+                          } else if (response.key === 'checked_out') {
+                              const button = $('<button type="button">')
+                                .addClass('btn btn-xs')
+                                .text(__('Check in and add to bundle'))
+                                .on('click', function () {
+                                    addToBundle(url, { external_id: barcode, force_checkin: true });
+                                });
+                              $('#addResult')
+                                .empty()
+                                .attr('class', 'alert alert-warning')
+                                .append(__x('Warning: Item {barcode} is checked out', { barcode }))
+                                .append(' ', button);
+                          } else if (response.key === 'failed_checkin') {
+                              $('#addResult')
+                                .empty()
+                                .attr('class', 'alert alert-danger')
+                                .append(__x('Failure: Item {barcode} cannot be checked in', { barcode }))
                           } else {
                               $('#addResult').replaceWith('<div id="addResult" class="alert alert-danger">'+_("Failure: Item '%s' belongs to another bundle").format(barcode)+'</div>');
                           }
@@ -1984,6 +1993,16 @@ Note that permanent location is a code, and location may be an authval.
                       }
                       $('#external_id').val('').focus();
                   });
+            }
+
+            $("#addToBundleForm").submit(function(event) {
+                  /* stop form from submitting normally */
+                  event.preventDefault();
+
+                  const url = this.action;
+                  const data = { external_id: this.elements.external_id.value };
+
+                  addToBundle(url, data);
             });
 
             $("#addToBundleModal").on("hidden.bs.modal", function(e){
index d9252ef..6f9d7ce 100755 (executable)
@@ -213,12 +213,17 @@ subtest 'bundle_host tests' => sub {
 };
 
 subtest 'add_to_bundle tests' => sub {
-    plan tests => 6;
+    plan tests => 7;
 
     $schema->storage->txn_begin;
 
     t::lib::Mocks::mock_preference( 'BundleNotLoanValue', 1 );
 
+    my $library = $builder->build_object({ class => 'Koha::Libraries' });
+    t::lib::Mocks::mock_userenv({
+        branchcode => $library->branchcode
+    });
+
     my $host_item = $builder->build_sample_item();
     my $bundle_item1 = $builder->build_sample_item();
     my $bundle_item2 = $builder->build_sample_item();
@@ -242,6 +247,26 @@ subtest 'add_to_bundle tests' => sub {
     'Koha::Exceptions::Item::Bundle::IsBundle',
       'Exception thrown if you try to add a bundle host to a bundle item';
 
+    my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
+    C4::Circulation::AddIssue( $patron->unblessed, $bundle_item2->barcode );
+    throws_ok { $host_item->add_to_bundle($bundle_item2) }
+    'Koha::Exceptions::Item::Bundle::ItemIsCheckedOut',
+      'Exception thrown if you try to add a checked out item';
+
+    $bundle_item2->withdrawn(1)->store;
+    t::lib::Mocks::mock_preference( 'BlockReturnOfWithdrawnItems', 1 );
+    throws_ok { $host_item->add_to_bundle( $bundle_item2, { force_checkin => 1 } ) }
+    'Koha::Exceptions::Checkin::FailedCheckin',
+      'Exception thrown if you try to add a checked out item using
+      "force_checkin" and the return is not possible';
+
+    $bundle_item2->withdrawn(0)->store;
+    lives_ok { $host_item->add_to_bundle( $bundle_item2, { force_checkin => 1 } ) }
+    'No exception if you try to add a checked out item using "force_checkin" and the return is possible';
+
+    $bundle_item2->discard_changes;
+    ok( !$bundle_item2->checkout, 'Item is not checked out after being added to a bundle' );
+
     $schema->storage->txn_rollback;
 };