From f3b7819033591728368443bc729b4dcf69eed248 Mon Sep 17 00:00:00 2001 From: Jonathan Druart Date: Tue, 25 Nov 2025 15:45:13 +0100 Subject: [PATCH] Bug 41254: Allow filtering by all authorised values for multi-valued statuses Some item statuses can correspond to multiple authorised values: Not for loan, Withdrawn, Lost and Damaged. Until now, only a single entry could be selected, and filtering applied to all values at once. This change introduces finer-grained filtering by exposing each authorised value individually in the filter. Test plan: Assign multiple authorised values to items on a bibliographic record for the four affected attributes: Not for loan, Withdrawn, Lost and Damaged. Use the "Status" filter and verify that each value behaves as expected. Display note: The current dropdown presentation is not ideal. Suggestions for improvement are welcome, although avoiding optgroup would be preferable to keep the scope small. Signed-off-by: Owen Leonard --- Koha/Items.pm | 15 ++++ .../tables/items/catalogue_detail.inc | 31 +++++++- t/db_dependent/Koha/Items.t | 71 ++++++++++++++++++- 3 files changed, 113 insertions(+), 4 deletions(-) diff --git a/Koha/Items.pm b/Koha/Items.pm index 862631fbfb..668d5f4bac 100644 --- a/Koha/Items.pm +++ b/Koha/Items.pm @@ -675,17 +675,32 @@ sub search { if ( $status eq 'lost' ) { $self = $self->search( { itemlost => { '!=' => 0 } } ); } + if ( $status =~ '^lost-(.*)' ) { + $self = $self->search( { itemlost => $1 } ); + } if ( $status eq 'withdrawn' ) { $self = $self->search( { withdrawn => { '!=' => 0 } } ); } + if ( $status =~ '^withdrawn-(.*)' ) { + $self = $self->search( { withdrawn => $1 } ); + } if ( $status eq 'damaged' ) { $self = $self->search( { damaged => { '!=' => 0 } } ); } + if ( $status =~ '^damaged-(.*)' ) { + $self = $self->search( { damaged => $1 } ); + } if ( $status eq 'not_for_loan' ) { my @item_types_notforloan = Koha::ItemTypes->search( { notforloan => { '!=' => 0 } } )->get_column('itemtype'); $self = $self->search( [ { notforloan => { '!=' => 0 } }, { 'me.itype' => \@item_types_notforloan } ] ); } + if ( $status =~ '^not_for_loan-(.*)' ) { + my $av_not_for_loan = $1; + my @item_types_notforloan = + Koha::ItemTypes->search( { notforloan => $av_not_for_loan } )->get_column('itemtype'); + $self = $self->search( [ { notforloan => $av_not_for_loan }, { 'me.itype' => \@item_types_notforloan } ] ); + } if ( $status eq 'on_hold' ) { $self = $self->filter_by_has_holds; } diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/html_helpers/tables/items/catalogue_detail.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/html_helpers/tables/items/catalogue_detail.inc index d74198bdaf..7ac2e38c01 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/html_helpers/tables/items/catalogue_detail.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/html_helpers/tables/items/catalogue_detail.inc @@ -218,9 +218,6 @@ }); const item_types_notforloan = new Map(all_item_types.map( it => [it.itemtype, it.notforloan] )); - const statuses = {checked_out: _("Checked out"), local_use: _("On-site checkout"), in_transit: _("In transit"), lost: _("Lost"), withdrawn: _("Withdrawn"), damaged:_("Damaged"), not_for_loan: _("Not for loan"), on_hold: _("On hold"), recalled: _("Recalled"), available: _("Available"), restricted: _("Restricted"), in_bundle: _("In bundle")}; - const all_statuses = Object.keys(statuses).map(k => {return {_id: k, _str: statuses[k]}}); - const can_edit_items_from = [% To.json(can_edit_items_from || []) | $raw %]; const item_type_image_locations = [% To.json(item_type_image_locations) | $raw %]; const av_loc = new Map([% To.json(AuthorisedValues.Get('LOC')) | $raw %].map( av => [av.authorised_value, av.lib])); @@ -240,6 +237,34 @@ location: new Map([% To.json(AuthorisedValues.GetDescriptionsByKohaField({ kohafield => 'items.location' })) | $raw %].map( av => [av.lib, av.authorised_value])), }; + const statuses = { + checked_out: _("Checked out"), + local_use: _("On-site checkout"), + in_transit: _("In transit"), + lost: _("Lost"), + ...Object.fromEntries( + [...av_lost].map(([k, v]) => [`lost-${k}`, `* ${v}`]) + ), + withdrawn: _("Withdrawn"), + ...Object.fromEntries( + [...av_withdrawn].map(([k, v]) => [`withdrawn-${k}`, `* ${v}`]) + ), + damaged:_("Damaged"), + ...Object.fromEntries( + [...av_damaged].map(([k, v]) => [`damaged-${k}`, `* ${v}`]) + ), + not_for_loan: _("Not for loan"), + ...Object.fromEntries( + [...av_not_for_loan].map(([k, v]) => [`not_for_loan-${k}`, `* ${v}`]) + ), + on_hold: _("On hold"), + recalled: _("Recalled"), + available: _("Available"), + restricted: _("Restricted"), + in_bundle: _("In bundle"), + }; + const all_statuses = Object.keys(statuses).map(k => {return {_id: k, _str: statuses[k]}}); + let filters_options = { item_types: all_item_types, libraries: all_libraries, diff --git a/t/db_dependent/Koha/Items.t b/t/db_dependent/Koha/Items.t index 3e74ac1fb6..6d230a7e14 100755 --- a/t/db_dependent/Koha/Items.t +++ b/t/db_dependent/Koha/Items.t @@ -75,7 +75,7 @@ is( $retrieved_item_1->barcode, $new_item_1->barcode, 'Find a item by id should subtest 'search' => sub { - plan tests => 9; + plan tests => 15; $schema->storage->txn_begin; my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); @@ -160,6 +160,75 @@ subtest 'search' => sub { is( $withdrawn_items->count, 1, "Filtered to 1 withdrawn item" ); is( $notforloan_items->count, 1, "Filtered to 1 notforloan item" ); + $item_6->notforloan(-1)->store; + is_deeply( + [ + Koha::Items->search( + { + _status => 'not_for_loan', + biblionumber => $biblio->biblionumber, + } + )->get_column('itemnumber') + ], + [ $item_6->itemnumber ] + ); + is_deeply( + [ + Koha::Items->search( + { + _status => 'not_for_loan-1', + biblionumber => $biblio->biblionumber, + } + )->get_column('itemnumber') + ], + [] + ); + is_deeply( + [ + Koha::Items->search( + { + _status => 'not_for_loan--1', + biblionumber => $biblio->biblionumber, + } + )->get_column('itemnumber') + ], + [ $item_6->itemnumber ] + ); + $item_6->notforloan(2)->store; + is_deeply( + [ + Koha::Items->search( + { + _status => 'not_for_loan', + biblionumber => $biblio->biblionumber, + } + )->get_column('itemnumber') + ], + [ $item_6->itemnumber ] + ); + is_deeply( + [ + Koha::Items->search( + { + _status => 'not_for_loan-1', + biblionumber => $biblio->biblionumber, + } + )->get_column('itemnumber') + ], + [] + ); + is_deeply( + [ + Koha::Items->search( + { + _status => 'not_for_loan-2', + biblionumber => $biblio->biblionumber, + } + )->get_column('itemnumber') + ], + [ $item_6->itemnumber ] + ); + C4::Circulation::AddIssue( $patron, $item_1->barcode ); my $checked_out_items = Koha::Items->search( -- 2.39.5