From af05a001193af0c89075aedbdaa36938aca5c94e Mon Sep 17 00:00:00 2001 From: Paul Derscheid Date: Thu, 19 Feb 2026 10:37:38 +0100 Subject: [PATCH] Bug 41886: Filter checkouts to bookable items in check_booking - Biblio::check_booking counts current checkouts toward the unavailable pool to detect booking clashes - It counts checkouts on ALL items for the biblio, including non-bookable ones (e.g. items with an excluded item type) - When only a subset of items are bookable, checkouts on non-bookable siblings incorrectly reduce the available pool, producing false clashes - Restrict the checkout query to only count items whose itemnumber is in the bookable items set To test: 1. Have a biblio with at least two items, where one item is bookable and the other is not (bookable = 0). 2. Check out the non-bookable item to a patron. 3. Attempt to create a booking for the bookable item. 4. Without the patch: a clash is detected even though the bookable item is available. 5. Apply the patch. 6. Repeat steps 2-3. 7. Verify the booking succeeds without a false clash. 8. Run: prove t/db_dependent/Koha/Biblio.t and confirm the new subtest "checkouts on non-bookable items do not cause false clashes" passes. Signed-off-by: David Nind --- Koha/Biblio.pm | 11 +++++++-- t/db_dependent/Koha/Biblio.t | 45 +++++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/Koha/Biblio.pm b/Koha/Biblio.pm index 67ea254c63..1f3f0b45e1 100644 --- a/Koha/Biblio.pm +++ b/Koha/Biblio.pm @@ -394,10 +394,17 @@ sub check_booking { ? $existing_bookings->search( { booking_id => { '!=' => $booking_id } } )->count : $existing_bookings->count; + # Only count checkouts on bookable items; checkouts on non-bookable + # sibling items should not reduce the pool of available bookable items. + my $bookable_item_ids = [ $bookable_items->reset->get_column('itemnumber') ]; + my $checkouts = $self->current_checkouts->search( { - date_due => { '>=' => $dtf->format_datetime($start_date) }, - "me.itemnumber" => { '-not_in' => $existing_bookings->_resultset->get_column('item_id')->as_query } + date_due => { '>=' => $dtf->format_datetime($start_date) }, + "me.itemnumber" => { + '-not_in' => $existing_bookings->_resultset->get_column('item_id')->as_query, + '-in' => $bookable_item_ids, + }, } ); $booked_count += $checkouts->count; diff --git a/t/db_dependent/Koha/Biblio.t b/t/db_dependent/Koha/Biblio.t index b4b282c248..48b8c607c8 100755 --- a/t/db_dependent/Koha/Biblio.t +++ b/t/db_dependent/Koha/Biblio.t @@ -2214,7 +2214,7 @@ sub host_record { } subtest 'check_booking tests' => sub { - plan tests => 6; + plan tests => 7; $schema->storage->txn_begin; @@ -2396,5 +2396,48 @@ subtest 'check_booking tests' => sub { is( $check_booking, 1, "Koha::Biblio->check_booking returns true when we can book on an item" ); + subtest 'checkouts on non-bookable items do not cause false clashes' => sub { + plan tests => 2; + + my $nb_biblio = $builder->build_sample_biblio(); + my $bookable_item = $builder->build_sample_item( { biblionumber => $nb_biblio->biblionumber, bookable => 1 } ); + my $non_bookable_item = + $builder->build_sample_item( { biblionumber => $nb_biblio->biblionumber, bookable => 0 } ); + + my $nb_patron = $builder->build_object( { class => 'Koha::Patrons' } ); + + # Check out the non-bookable item + AddIssue( $nb_patron, $non_bookable_item->barcode ); + + my $nb_start = dt_from_string()->truncate( to => 'day' ); + my $nb_end = $nb_start->clone->add( days => 7 ); + + my $can_book = $nb_biblio->check_booking( + { + start_date => $nb_start, + end_date => $nb_end, + } + ); + is( + $can_book, 1, + "Checkout on non-bookable item does not reduce availability" + ); + + # Now also check out the bookable item to confirm it + # correctly detects unavailability + AddIssue( $nb_patron, $bookable_item->barcode ); + + $can_book = $nb_biblio->check_booking( + { + start_date => $nb_start, + end_date => $nb_end, + } + ); + is( + $can_book, 0, + "Checkout on bookable item correctly reduces availability" + ); + }; + $schema->storage->txn_rollback; }; -- 2.39.5