Bugzilla – Attachment 184974 Details for
Bug 37334
Cannot filter holdings table by status
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 37334: [24.11] Allow filter holdings table by status (squashed)
Bug-37334-2411-Allow-filter-holdings-table-by-stat.patch (text/plain), 40.58 KB, created by
Fridolin Somers
on 2025-08-01 08:56:49 UTC
(
hide
)
Description:
Bug 37334: [24.11] Allow filter holdings table by status (squashed)
Filename:
MIME Type:
Creator:
Fridolin Somers
Created:
2025-08-01 08:56:49 UTC
Size:
40.58 KB
patch
obsolete
>From 2d5d29e1647d6a2dc3d3bb22c77f285383c01f8d Mon Sep 17 00:00:00 2001 >From: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com> >Date: Thu, 31 Jul 2025 12:26:23 +0000 >Subject: [PATCH] Bug 37334: [24.11] Allow filter holdings table by status > (squashed) > >--- > Koha/Item.pm | 49 ++ > Koha/Items.pm | 198 ++++++++ > api/v1/swagger/definitions/item.yaml | 7 +- > api/v1/swagger/paths/biblios.yaml | 3 +- > .../tables/items/catalogue_detail.inc | 57 ++- > koha-tmpl/intranet-tmpl/prog/js/datatables.js | 9 +- > t/db_dependent/Koha/Item.t | 140 +++++- > t/db_dependent/Koha/Items.t | 469 +++++++++++++++++- > 8 files changed, 900 insertions(+), 32 deletions(-) > >diff --git a/Koha/Item.pm b/Koha/Item.pm >index 0de5531f1e..c98bdd5f17 100644 >--- a/Koha/Item.pm >+++ b/Koha/Item.pm >@@ -1443,6 +1443,55 @@ sub columns_to_str { > return $values; > } > >+sub _status { >+ my ($self) = @_; >+ >+ my @statuses; >+ if ( my $checkout = $self->checkout ) { >+ unless ( $checkout->onsite_checkout ) { >+ push @statuses, "checked_out"; >+ } else { >+ push @statuses, "local_use"; >+ } >+ } elsif ( my $transfer = $self->transfer ) { >+ push @statuses, "in_transit"; >+ } >+ if ( $self->itemlost ) { >+ push @statuses, 'lost'; >+ } >+ if ( $self->withdrawn ) { >+ push @statuses, 'withdrawn'; >+ } >+ if ( $self->damaged ) { >+ push @statuses, 'damaged'; >+ } >+ if ( $self->notforloan || $self->item_type->notforloan ) { >+ >+ # TODO on a big Koha::Items loop we are going to join with item_type too often, use a cache >+ push @statuses, 'not_for_loan'; >+ } >+ if ( $self->first_hold ) { >+ push @statuses, 'on_hold'; >+ } >+ if ( C4::Context->preference('UseRecalls') && $self->recall ) { >+ push @statuses, 'recalled'; >+ } >+ >+ unless (@statuses) { >+ push @statuses, 'available'; >+ } >+ >+ if ( $self->restricted ) { >+ push @statuses, 'restricted'; >+ } >+ >+ if ( $self->in_bundle ) { >+ push @statuses, 'in_bundle'; >+ } >+ >+ return join ',', @statuses; >+} >+ > =head3 additional_attributes > > my $attributes = $item->additional_attributes; >diff --git a/Koha/Items.pm b/Koha/Items.pm >index 1cb1dce1b5..d7744cbbc6 100644 >--- a/Koha/Items.pm >+++ b/Koha/Items.pm >@@ -222,6 +222,148 @@ sub filter_by_bookable { > ); > } > >+=head3 filter_by_checked_out >+ >+ my $checked_out_items = $items->filter_by_checked_out; >+ >+Returns a new resultset, containing only those items that are currently checked out. >+ >+=cut >+ >+sub filter_by_checked_out { >+ my ( $self, $params ) = @_; >+ >+ $params //= {}; >+ my $checkouts = Koha::Checkouts->search( >+ { %$params, 'me.itemnumber' => [ $self->get_column('itemnumber') ], }, >+ { >+ columns => ['itemnumber'], >+ distinct => 1 >+ } >+ )->_resultset->as_query; >+ >+ return $self->search( { 'me.itemnumber' => { '-in' => $checkouts } } ); >+} >+ >+=head3 filter_by_in_transit >+ >+ my $in_tranist_items = $items->filter_by_in_transit; >+ >+Returns a new resultset, containing only those items that are currently in transit. >+ >+=cut >+ >+sub filter_by_in_transit { >+ my ( $self, $params ) = @_; >+ >+ $params //= {}; >+ my $transfers = Koha::Item::Transfers->search( >+ { %$params, 'me.itemnumber' => [ $self->get_column('itemnumber') ], }, >+ { >+ columns => ['itemnumber'], >+ distinct => 1 >+ } >+ )->_resultset->as_query; >+ >+ return $self->search( { 'me.itemnumber' => { '-in' => $transfers } } ); >+} >+ >+=head3 filter_by_has_holds >+ >+ my $has_hold_items = $items->filter_by_has_holds; >+ >+Returns a new resultset, containing only those items that currently have holds. >+ >+=cut >+ >+sub filter_by_has_holds { >+ my ( $self, $params ) = @_; >+ >+ $params //= {}; >+ my $holds = Koha::Holds->search( >+ { %$params, 'me.itemnumber' => [ $self->get_column('itemnumber') ], }, >+ { >+ columns => ['itemnumber'], >+ distinct => 1 >+ } >+ )->_resultset->as_query; >+ >+ return $self->search( { 'me.itemnumber' => { '-in' => $holds } } ); >+} >+ >+=head3 filter_by_has_recalls >+ >+ my $has_recalls_items = $items->filter_by_has_recalls; >+ >+Returns a new resultset, containing only those items that currently have recalls. >+ >+=cut >+ >+sub filter_by_has_recalls { >+ my ( $self, $params ) = @_; >+ >+ $params //= {}; >+ my $recalls = Koha::Recalls->search( >+ { %$params, 'me.itemnumber' => [ $self->get_column('itemnumber') ], 'me.item_level' => 1, }, >+ { >+ columns => ['itemnumber'], >+ distinct => 1 >+ } >+ )->_resultset->as_query; >+ return $self->search( { 'me.itemnumber' => { '-in' => $recalls } } ); >+} >+ >+=head3 filter_by_in_bundle >+ >+Returns a new resultset, containing only those items that currently are part of a bundle. >+ >+=cut >+ >+sub filter_by_in_bundle { >+ my ($self) = @_; >+ >+ my @in_bundle_items; >+ while ( my $item = $self->next ) { >+ push @in_bundle_items, $item if $item->in_bundle; >+ } >+ >+ my @bundled_items = map { $_->itemnumber } @in_bundle_items; >+ return $self->search( { 'me.itemnumber' => { '-in' => \@bundled_items } } ); >+} >+ >+=head3 filter_by_available >+ >+ my $available_items = $items->filter_by_available; >+ >+Returns a new resultset, containing only those items that are currently available. >+ >+=cut >+ >+sub filter_by_available { >+ my ($self) = @_; >+ >+ my @all_itemnumbers = $self->get_column('itemnumber'); >+ my @not_available_itemnumbers; >+ push @not_available_itemnumbers, $self->filter_by_checked_out->get_column('itemnumber'); >+ push @not_available_itemnumbers, $self->filter_by_in_transit->get_column('itemnumber'); >+ >+ push @not_available_itemnumbers, $self->filter_by_has_holds->get_column('itemnumber'); >+ push @not_available_itemnumbers, $self->filter_by_has_recalls->get_column('itemnumber'); >+ >+ my @item_types_notforloan = Koha::ItemTypes->search( { notforloan => { '!=' => 0 } } )->get_column('itemtype'); >+ return Koha::Items->search( >+ { >+ 'me.itemnumber' => [ array_minus @all_itemnumbers, @not_available_itemnumbers ], >+ itemlost => 0, >+ withdrawn => 0, >+ damaged => 0, >+ notforloan => 0, >+ restricted => [ { '!=' => 0 }, undef ], >+ 'me.itype' => { -not_in => \@item_types_notforloan }, >+ } >+ ); >+} >+ > =head3 move_to_biblio > > $items->move_to_biblio($to_biblio); >@@ -485,6 +627,62 @@ sub apply_regex { > return $value; > } > >+=head3 search >+ >+ my $search_result = $object->search( $params, $attributes ); >+ >+Filters items based on the specified status. >+ >+=cut >+ >+sub search { >+ my ( $self, $params, $attributes ) = @_; >+ my $status = ( $params && ref($params) eq 'HASH' ) ? delete $params->{_status} : undef; >+ if ($status) { >+ if ( $status eq 'checked_out' ) { >+ $self = $self->filter_by_checked_out( { onsite_checkout => 0 } ); >+ } >+ if ( $status eq 'local_use' ) { >+ $self = $self->filter_by_checked_out( { onsite_checkout => 1 } ); >+ } >+ if ( $status eq 'in_transit' ) { >+ $self = $self->filter_by_in_transit; >+ } >+ if ( $status eq 'lost' ) { >+ $self = $self->search( { itemlost => { '!=' => 0 } } ); >+ } >+ if ( $status eq 'withdrawn' ) { >+ $self = $self->search( { withdrawn => { '!=' => 0 } } ); >+ } >+ if ( $status eq 'damaged' ) { >+ $self = $self->search( { damaged => { '!=' => 0 } } ); >+ } >+ 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 eq 'on_hold' ) { >+ $self = $self->filter_by_has_holds; >+ } >+ if ( $status eq 'recalled' ) { >+ $self = $self->filter_by_has_recalls; >+ } >+ if ( $status eq 'in_bundle' ) { >+ $self = $self->filter_by_in_bundle; >+ } >+ >+ if ( $status eq 'available' ) { >+ $self = $self->filter_by_available; >+ } >+ >+ if ( $status eq 'restricted' ) { >+ $self = $self->search( { restricted => [ { '!=' => 0 } ] } ); >+ } >+ } >+ return $self->SUPER::search( $params, $attributes ); >+} >+ > =head3 search_ordered > > $items->search_ordered; >diff --git a/api/v1/swagger/definitions/item.yaml b/api/v1/swagger/definitions/item.yaml >index b254eb2dc3..4df212b8db 100644 >--- a/api/v1/swagger/definitions/item.yaml >+++ b/api/v1/swagger/definitions/item.yaml >@@ -322,4 +322,9 @@ properties: > - object > - "null" > description: A return claims object if one exists that's unresolved >-additionalProperties: false >+ _status: >+ type: >+ - string >+ - "null" >+ description: The status of the item >+additionalProperties: false >\ No newline at end of file >diff --git a/api/v1/swagger/paths/biblios.yaml b/api/v1/swagger/paths/biblios.yaml >index 4f301fc911..ab9f8ebd45 100644 >--- a/api/v1/swagger/paths/biblios.yaml >+++ b/api/v1/swagger/paths/biblios.yaml >@@ -458,6 +458,7 @@ > type: string > enum: > - +strings >+ - _status > - home_library > - holding_library > - biblio.title >@@ -918,4 +919,4 @@ > "503": > description: Under maintenance > schema: >- $ref: "../swagger.yaml#/definitions/error" >+ $ref: "../swagger.yaml#/definitions/error" >\ No newline at end of file >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 771de2ea12..9215daeeb6 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 >@@ -220,6 +220,9 @@ > }); > 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]}}).sort(); >+ > 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])); >@@ -247,7 +250,7 @@ > [%# In case or SeparateHoldings we may need to display the number of biblios in each tab %] > [%# Do we need separate/new endpoints or do we hack the somewhere client-side? %] > let item_table_url = "/api/v1/biblios/[% biblio.biblionumber | uri %]/items?"; >- let embed = ["+strings,checkout,checkout.patron,transfer,transfer+strings,first_hold,first_hold+strings,first_hold.patron,first_hold.desk"]; >+ let embed = ["+strings,_status,checkout,checkout.patron,transfer,transfer+strings,first_hold,first_hold+strings,first_hold.patron,first_hold.desk"]; > [% IF Koha.Preference('LocalCoverImages') %] > embed.push('cover_image_ids'); > [% END %] >@@ -319,12 +322,17 @@ > items_selection[tab_id] = []; > } > >+ default_filters._status = function(){ >+ return $("#" + tab_id + "_status select").val(); >+ }; >+ > let offset = 2; > [% UNLESS Koha.Preference('LocalCoverImages') %]offset--;[% END %] > let filters_options = { > [offset] : () => all_item_types, > [offset+1] : () => all_libraries, > [offset+2] : () => all_libraries, >+ [offset+6] : () => all_statuses, > }; > > var items_table = $("#" + tab_id + '_table').kohaTable({ >@@ -490,29 +498,31 @@ > } > }, > { >- data: "me.lost_status", >+ data: "", > className: "status", >- searchable: false, // FIXME We are losing the ability to search on the status >+ searchable: false, > orderable: false, > render: function (data, type, row, meta) { > let nodes = ""; >- if ( row.checkout ) { >+ row._status.split(",").forEach( status => { >+ if ( status == 'checked_out' || status == 'local_use') { >+ nodes += '<span>'; >+ > [%# Hacky for patron_to_html in case we simply want to display the patron's library name %] > row.checkout.patron.library = { name: libraries_names.get(row.checkout.patron.library_id) }; >+ let patron_to_html = $patron_to_html(row.checkout.patron, { url: true, display_cardnumber: true, hide_patron_name }); > >- nodes += '<span>'; >- if ( row.checkout.onsite_checkout ) { >- let patron_to_html = $patron_to_html(row.checkout.patron, { url: true, display_cardnumber: true, hide_patron_name }); >+ if ( status == 'local_use' ) { > nodes += _("Currently in local use by %s").format(patron_to_html); > } else { > nodes += '<span class="datedue">'; >- let patron_to_html = $patron_to_html(row.checkout.patron, { url: true, display_cardnumber: true, hide_patron_name }); > nodes += _("Checked out to %s").format(patron_to_html); > } > nodes += ': '; > nodes += _("due %s").format($date(row.checkout.due_date, { as_due_date: true })); > nodes += "</span>" >- } else if ( row.transfer ) { >+ } >+ if ( status == 'in_transit' ) { > if ( row.transfer.datesent ) { > nodes += '<span class="intransit">%s</span>'.format(_("In transit from %s to %s since %s").format(escape_str(row.transfer._strings.from_library.str), escape_str(row.transfer._strings.to_library.str), $date(row.transfer.datesent))); > } else { >@@ -520,33 +530,31 @@ > } > } > >- if ( row.lost_status ) { >+ if ( status == 'lost' ) { > let lost_lib = av_lost.get(row.lost_status.toString()) || _("Unavailable (lost or missing"); > nodes += '<span class="lost">%s</span>'.format(escape_str(lost_lib)); >- [% IF Koha.Preference('ClaimReturnedLostValue') %] >- const hasReturnClaims = row.return_claims.filter(rc => !rc.resolution).length > 0 ? true : false >- if(hasReturnClaims) { >- nodes += '<span class="claimed_returned">' + _("(Claimed returned)") + '</span>'; >- } >- [% END %] >+ const hasReturnClaims = row.return_claims && row.return_claims.filter(rc => !rc.resolution).length > 0 ? true : false >+ if(hasReturnClaims) { >+ nodes += '<span class="claimed_returned">' + _("(Claimed returned)") + '</span>'; >+ } > } > >- if ( row.withdrawn ) { >+ if ( status == 'withdrawn' ) { > let withdrawn_lib = av_withdrawn.get(row.withdrawn.toString()) || _("Withdrawn"); > nodes += '<span class="wdn">%s</span>'.format(escape_str(withdrawn_lib)); > } > >- if ( row.damaged_status ) { >+ if ( status == 'damaged' ) { > let damaged_lib = av_damaged.get(row.damaged_status.toString()) || _("Damaged"); > nodes += '<span class="dmg">%s</span>'.format(escape_str(damaged_lib)); > } > >- if ( row.not_for_loan_status || item_types_notforloan.get(row.item_type_id) ) { >+ if ( status == 'not_for_loan' ) { > let not_for_loan_lib = av_not_for_loan.get(row.not_for_loan_status.toString()); > nodes += '<span class="notforloan">%s'.format(_("Not for loan")) + ( not_for_loan_lib ? '<span class="reason"> (%s)</span>'.format(escape_str(not_for_loan_lib)) : '' ) + '</span>'; > } > >- if ( row.first_hold ) { >+ if ( status == 'on_hold') { > if ( row.first_hold.waiting_date ) { > if ( row.first_hold.desk ) { > nodes += '<span class="waitingat">%s</span>'.format(_("Waiting at %s, %s since %s.").format(row.first_hold._strings.pickup_library_id.str, row.first_hold.desk.desk_name, $date(row.first_hold.waiting_date))); >@@ -580,18 +588,17 @@ > } > } > [% END %] >- >- if ( ! ( row.not_for_loan_status || item_types_notforloan.get(row.item_type_id) || row.checked_out_date || row.lost_status || row.withdrawn || row.damaged_status || row.transfer || row.first_hold || ( row.recall && ( row.item_id === row.recall.item_id ) ) )) { >+ if ( status == 'available' ) { > nodes += ' <span>%s</span>'.format(_("Available")) > } > >- if ( row.restricted_status ) { >+ if ( status == 'restricted') { > nodes += '<span class="restricted">(%s)</span>'.format(escape_str(av_restricted.get(row.restricted_status.toString()))); > } >- >- if ( row.in_bundle ) { >+ if ( status == 'in_bundle') { > nodes += '<span class="bundled">%s</span>'.format(_("In bundle: %s").format($biblio_to_html(row.bundle_host.biblio, { link: true }))); > } >+ }); //end forEach > return nodes; > } > }, >diff --git a/koha-tmpl/intranet-tmpl/prog/js/datatables.js b/koha-tmpl/intranet-tmpl/prog/js/datatables.js >index 154144f441..f0af7c86cf 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/datatables.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/datatables.js >@@ -606,7 +606,9 @@ function _dt_default_ajax (params){ > } else if ( f == '-and' ) { > if (v) and_query_parameters.push(v) > } else if ( v ) { >- additional_filters[k] = v; >+ additional_filters[k] = v >+ .replace(/^\^/, "") >+ .replace(/\$$/, ""); > } > } > if ( Object.keys(additional_filters).length ) { >@@ -883,9 +885,10 @@ function _dt_add_filters(table_node, table_dt, filters_options = {}) { > let i = column.index(); > var visible_i = table_dt.column.index('fromData', i); > let th = $(table_node).find('thead tr:eq(1) th:eq(%s)'.format(visible_i)); >- var is_searchable = columns[i].bSearchable; >+ var is_searchable = table_dt.settings()[0].aoColumns[i].bSearchable; > $(th).removeClass('sorting').removeClass("sorting_asc").removeClass("sorting_desc"); >- if ( is_searchable ) { >+ $(this).data("th-id", i); >+ if (is_searchable || $(this).data("filter") || filters_options[i]) { > let input_type = 'input'; > let existing_search = column.search(); > if ( $(th).data('filter') || filters_options.hasOwnProperty(i)) { >diff --git a/t/db_dependent/Koha/Item.t b/t/db_dependent/Koha/Item.t >index c2778b5c8c..e8afaede53 100755 >--- a/t/db_dependent/Koha/Item.t >+++ b/t/db_dependent/Koha/Item.t >@@ -20,7 +20,7 @@ > use Modern::Perl; > use utf8; > >-use Test::More tests => 38; >+use Test::More tests => 39; > use Test::Exception; > use Test::MockModule; > use Test::Warn; >@@ -45,6 +45,144 @@ use t::lib::Dates; > my $schema = Koha::Database->new->schema; > my $builder = t::lib::TestBuilder->new; > >+subtest '_status' => sub { >+ plan tests => 12; >+ >+ $schema->storage->txn_begin; >+ >+ my $item = $builder->build_sample_item(); >+ my $library = $builder->build_object( { class => 'Koha::Libraries' } ); >+ t::lib::Mocks::mock_userenv( { branchcode => $library->branchcode } ); >+ >+ t::lib::Mocks::mock_preference( 'UseRecalls', 1 ); >+ >+ my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); >+ >+ my @test_cases = ( >+ { >+ setup => sub { >+ my $onloan_item = $builder->build_sample_item(); >+ AddIssue( $patron, $onloan_item->barcode, dt_from_string ); >+ return $onloan_item; >+ }, >+ expected_status => 'checked_out', >+ description => 'Checked out status correctly returned', >+ }, >+ { >+ setup => sub { >+ my $onsite_item = $builder->build_sample_item(); >+ AddIssue( >+ $patron, $onsite_item->barcode, dt_from_string, undef, undef, undef, >+ { onsite_checkout => 1 } >+ ); >+ return $onsite_item; >+ }, >+ expected_status => 'local_use', >+ description => 'Local use status correctly returned', >+ }, >+ { >+ setup => sub { >+ return $item; >+ }, >+ expected_status => 'available', >+ description => 'Available status correctly returned', >+ }, >+ { >+ setup => sub { >+ $item->itemlost(1)->store(); >+ return $item; >+ }, >+ expected_status => 'lost', >+ description => 'Lost status correctly returned', >+ }, >+ { >+ setup => sub { >+ $item->withdrawn(1)->store(); >+ return $item; >+ }, >+ expected_status => qr/lost,withdrawn/, >+ description => 'Lost and withdrawn status correctly returned', >+ }, >+ { >+ setup => sub { >+ $item->damaged(1)->store(); >+ return $item; >+ }, >+ expected_status => qr/lost,withdrawn,damaged/, >+ description => 'Lost, withdrawn, and damaged status correctly returned', >+ }, >+ { >+ setup => sub { >+ $item->notforloan(1)->store(); >+ return $item; >+ }, >+ expected_status => 'not_for_loan', >+ description => 'Positive not_for_loan status correctly returned', >+ }, >+ { >+ setup => sub { >+ $item->notforloan(-1)->store(); >+ return $item; >+ }, >+ expected_status => 'not_for_loan', >+ description => 'Negative not_for_loan status correctly returned', >+ }, >+ { >+ setup => sub { >+ my $itemtype = $builder->build_object( { class => "Koha::ItemTypes", value => { notforloan => 1 } } ); >+ my $notforloan_item = $builder->build_sample_item( { itype => $itemtype->itemtype, } ); >+ return $notforloan_item; >+ }, >+ expected_status => 'not_for_loan', >+ description => 'Item type not_for_loan status correctly returned', >+ }, >+ { >+ setup => sub { >+ my $onhold_item = $builder->build_sample_item(); >+ C4::Reserves::AddReserve( >+ { >+ branchcode => $library->branchcode, >+ borrowernumber => $patron->borrowernumber, >+ biblionumber => $onhold_item->biblionumber, >+ itemnumber => $onhold_item->itemnumber, >+ } >+ ); >+ return $onhold_item; >+ }, >+ expected_status => 'on_hold', >+ description => 'On hold status correctly returned', >+ }, >+ { >+ setup => sub { >+ my $recalled_item = $builder->build_sample_item(); >+ AddIssue( $patron, $recalled_item->barcode, dt_from_string ); >+ Koha::Recalls->add_recall( >+ { biblio => $recalled_item->biblio, item => $recalled_item, patron => $patron } ); >+ return $recalled_item; >+ }, >+ expected_status => 'recalled', >+ description => 'Recalled status correctly returned', >+ }, >+ { >+ setup => sub { >+ $item->restricted(1)->store(); >+ return $item; >+ }, >+ expected_status => 'restricted', >+ description => 'Restricted status correctly returned', >+ }, >+ ); >+ >+ foreach my $test_case (@test_cases) { >+ my $item = $test_case->{setup}->(); >+ ok( $item->_status() =~ /$test_case->{expected_status}/, $test_case->{description} ); >+ } >+ >+ t::lib::Mocks::mock_preference( 'UseRecalls', 0 ); >+ >+ $schema->storage->txn_rollback; >+}; >+ > subtest 'z3950_status' => sub { > plan tests => 9; > >diff --git a/t/db_dependent/Koha/Items.t b/t/db_dependent/Koha/Items.t >index 03b26550eb..038db7bc5b 100755 >--- a/t/db_dependent/Koha/Items.t >+++ b/t/db_dependent/Koha/Items.t >@@ -19,7 +19,7 @@ > > use Modern::Perl; > >-use Test::More tests => 20; >+use Test::More tests => 27; > > use Test::MockModule; > use Test::Exception; >@@ -34,6 +34,7 @@ use Koha::Items; > use Koha::Database; > use Koha::DateUtils qw( dt_from_string ); > use Koha::Statistics; >+use Koha::Recalls; > > use t::lib::TestBuilder; > use t::lib::Mocks; >@@ -68,6 +69,163 @@ is( Koha::Items->search->count, $nb_of_items + 2, 'The 2 items should have been > my $retrieved_item_1 = Koha::Items->find( $new_item_1->itemnumber ); > is( $retrieved_item_1->barcode, $new_item_1->barcode, 'Find a item by id should return the correct item' ); > >+subtest 'search' => sub { >+ >+ plan tests => 9; >+ $schema->storage->txn_begin; >+ >+ my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); >+ my $patron_2 = $builder->build_object( { class => 'Koha::Patrons' } ); >+ t::lib::Mocks::mock_userenv( { branchcode => $patron->branchcode } ); >+ >+ my $library_1 = $builder->build( { source => 'Branch' } ); >+ my $library_2 = $builder->build( { source => 'Branch' } ); >+ >+ my $biblio = $builder->build_sample_biblio(); >+ >+ my $item_1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, } ); >+ my $item_2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, } ); >+ >+ my $available_items = Koha::Items->search( >+ { >+ _status => 'available', >+ biblionumber => $biblio->biblionumber, >+ } >+ ); >+ >+ ok( $available_items->count == 2, "Filtered to 2 available items" ); >+ >+ my $item_3 = $builder->build_sample_item( >+ { >+ biblionumber => $biblio->biblionumber, >+ itemlost => 1, >+ } >+ ); >+ >+ my $item_4 = $builder->build_sample_item( >+ { >+ biblionumber => $biblio->biblionumber, >+ damaged => 1, >+ } >+ ); >+ >+ my $item_5 = $builder->build_sample_item( >+ { >+ biblionumber => $biblio->biblionumber, >+ withdrawn => 1, >+ } >+ ); >+ >+ my $item_6 = $builder->build_sample_item( >+ { >+ biblionumber => $biblio->biblionumber, >+ notforloan => 1, >+ } >+ ); >+ >+ my $lost_items = Koha::Items->search( >+ { >+ _status => 'lost', >+ biblionumber => $biblio->biblionumber, >+ } >+ ); >+ >+ my $damaged_items = Koha::Items->search( >+ { >+ _status => 'damaged', >+ biblionumber => $biblio->biblionumber, >+ } >+ ); >+ >+ my $withdrawn_items = Koha::Items->search( >+ { >+ _status => 'withdrawn', >+ biblionumber => $biblio->biblionumber, >+ } >+ ); >+ >+ my $notforloan_items = Koha::Items->search( >+ { >+ _status => 'not_for_loan', >+ biblionumber => $biblio->biblionumber, >+ } >+ ); >+ >+ ok( $lost_items->count == 1, "Filtered to 1 lost item" ); >+ ok( $damaged_items->count == 1, "Filtered to 1 damaged item" ); >+ ok( $withdrawn_items->count == 1, "Filtered to 1 withdrawn item" ); >+ ok( $notforloan_items->count == 1, "Filtered to 1 notforloan item" ); >+ >+ C4::Circulation::AddIssue( $patron, $item_1->barcode ); >+ >+ my $checked_out_items = Koha::Items->search( >+ { >+ _status => 'checked_out', >+ biblionumber => $biblio->biblionumber, >+ } >+ ); >+ >+ ok( $checked_out_items->count == 1, "Filtered to 1 checked out item" ); >+ >+ my $transfer_1 = $builder->build_object( >+ { >+ class => 'Koha::Item::Transfers', >+ value => { >+ itemnumber => $item_2->itemnumber, >+ frombranch => $library_1->{branchcode}, >+ tobranch => $library_2->{branchcode}, >+ } >+ } >+ ); >+ >+ my $in_transit_items = Koha::Items->search( >+ { >+ _status => 'in_transit', >+ biblionumber => $biblio->biblionumber, >+ } >+ ); >+ >+ ok( $in_transit_items->count == 1, "Filtered to 1 in transit item" ); >+ >+ my $item_7 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, } ); >+ >+ my $hold_1 = $builder->build( >+ { >+ source => 'Reserve', >+ value => { >+ itemnumber => $item_7->itemnumber, reservedate => dt_from_string, >+ } >+ } >+ ); >+ >+ my $on_hold_items = Koha::Items->search( >+ { >+ _status => 'on_hold', >+ biblionumber => $biblio->biblionumber, >+ } >+ ); >+ >+ ok( $on_hold_items->count == 1, "Filtered to 1 on hold item" ); >+ >+ my $item_8 = $builder->build_sample_item( >+ { >+ biblionumber => $biblio->biblionumber, >+ restricted => 1, >+ } >+ ); >+ >+ my $restricted_items = Koha::Items->search( >+ { >+ _status => 'restricted', >+ biblionumber => $biblio->biblionumber, >+ } >+ ); >+ >+ ok( $restricted_items->count == 1, "Filtered to 1 restricted item" ); >+ >+ $schema->storage->txn_rollback; >+}; >+ > subtest 'store' => sub { > plan tests => 8; > >@@ -2222,3 +2380,312 @@ subtest 'filter_by_bookable' => sub { > > $schema->storage->txn_rollback; > }; >+ >+subtest 'filter_by_checked_out' => sub { >+ plan tests => 4; >+ >+ $schema->storage->txn_begin; >+ >+ my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); >+ t::lib::Mocks::mock_userenv( { branchcode => $patron->branchcode } ); >+ >+ my $biblio = $builder->build_sample_biblio(); >+ my $item_1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, } ); >+ my $item_2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, } ); >+ >+ is( $biblio->items->filter_by_checked_out->count, 0, "Filtered 0 checked out items" ); >+ >+ C4::Circulation::AddIssue( $patron, $item_1->barcode ); >+ >+ is( $biblio->items->filter_by_checked_out->count, 1, "Filtered 1 checked out items" ); >+ >+ C4::Circulation::AddIssue( $patron, $item_2->barcode ); >+ >+ is( $biblio->items->filter_by_checked_out->count, 2, "Filtered 2 checked out items" ); >+ >+ # Do the returns >+ C4::Circulation::AddReturn( $item_1->barcode ); >+ C4::Circulation::AddReturn( $item_2->barcode ); >+ >+ is( $biblio->items->filter_by_checked_out->count, 0, "Filtered 0 checked out items" ); >+ >+ $schema->storage->txn_rollback; >+ >+}; >+ >+subtest 'filter_by_in_transit' => sub { >+ plan tests => 3; >+ >+ $schema->storage->txn_begin; >+ >+ my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); >+ t::lib::Mocks::mock_userenv( { branchcode => $patron->branchcode } ); >+ >+ my $library_1 = $builder->build( { source => 'Branch' } ); >+ my $library_2 = $builder->build( { source => 'Branch' } ); >+ >+ my $biblio = $builder->build_sample_biblio(); >+ my $item_1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, } ); >+ my $item_2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, } ); >+ >+ is( $biblio->items->filter_by_in_transit->count, 0, "Filtered 0 in transit items" ); >+ >+ my $transfer_1 = $builder->build_object( >+ { >+ class => 'Koha::Item::Transfers', >+ value => { >+ itemnumber => $item_1->itemnumber, >+ frombranch => $library_1->{branchcode}, >+ tobranch => $library_2->{branchcode}, >+ } >+ } >+ ); >+ >+ is( $biblio->items->filter_by_in_transit->count, 1, "Filtered 1 in transit items" ); >+ >+ my $transfer_2 = $builder->build_object( >+ { >+ class => 'Koha::Item::Transfers', >+ value => { >+ itemnumber => $item_2->itemnumber, >+ frombranch => $library_2->{branchcode}, >+ tobranch => $library_1->{branchcode}, >+ } >+ } >+ ); >+ >+ is( $biblio->items->filter_by_in_transit->count, 2, "Filtered 2 in transit items" ); >+ >+ $schema->storage->txn_rollback; >+ >+}; >+ >+subtest 'filter_by_has_holds' => sub { >+ plan tests => 3; >+ >+ $schema->storage->txn_begin; >+ >+ my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); >+ t::lib::Mocks::mock_userenv( { branchcode => $patron->branchcode } ); >+ >+ my $library_1 = $builder->build( { source => 'Branch' } ); >+ my $library_2 = $builder->build( { source => 'Branch' } ); >+ >+ my $biblio = $builder->build_sample_biblio(); >+ my $item_1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, } ); >+ my $item_2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, } ); >+ >+ is( $biblio->items->filter_by_has_holds->count, 0, "Filtered to 0 holds" ); >+ >+ my $hold_1 = $builder->build( >+ { >+ source => 'Reserve', >+ value => { >+ itemnumber => $item_1->itemnumber, reservedate => dt_from_string, >+ } >+ } >+ ); >+ >+ is( $biblio->items->filter_by_has_holds->count, 1, "Filtered to 1 hold" ); >+ >+ my $hold_2 = $builder->build( >+ { >+ source => 'Reserve', >+ value => { >+ itemnumber => $item_2->itemnumber, reservedate => dt_from_string, >+ } >+ } >+ ); >+ >+ is( $biblio->items->filter_by_has_holds->count, 2, "Filtered to 2 holds" ); >+ >+ $schema->storage->txn_rollback; >+ >+}; >+ >+subtest 'filter_by_in_bundle' => sub { >+ plan tests => 3; >+ >+ $schema->storage->txn_begin; >+ >+ my $library = $builder->build( { source => 'Branch' } ); >+ my $biblio = $builder->build_sample_biblio(); >+ >+ my $item_1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, } ); >+ my $item_2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, } ); >+ my $item_3 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, } ); >+ >+ is( $biblio->items->filter_by_in_bundle->count, 0, "0 items in a bundle for this record" ); >+ >+ my $in_bundle = $item_1->in_bundle; >+ >+ my $host_item = $builder->build_sample_item(); >+ $schema->resultset('ItemBundle')->create( { host => $host_item->itemnumber, item => $item_1->itemnumber } ); >+ >+ $in_bundle = $item_1->in_bundle; >+ >+ is( $biblio->items->filter_by_in_bundle->count, 1, "1 item in a bundle for this record" ); >+ $schema->resultset('ItemBundle')->create( { host => $host_item->itemnumber, item => $item_2->itemnumber } ); >+ >+ $in_bundle = $item_2->in_bundle; >+ >+ is( $biblio->items->filter_by_in_bundle->count, 2, "2 items in a bundle for this record" ); >+ >+ $schema->storage->txn_rollback; >+ >+}; >+ >+subtest 'filter_by_has_recalls' => sub { >+ plan tests => 2; >+ >+ $schema->storage->txn_begin; >+ >+ my $library = $builder->build_object( { class => 'Koha::Libraries' } ); >+ >+ $biblio = $builder->build_sample_biblio( { author => 'Hall, Daria' } ); >+ my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); >+ t::lib::Mocks::mock_userenv( { branchcode => $patron->branchcode } ); >+ >+ my $item = $builder->build_sample_item( >+ { >+ biblionumber => $biblio->biblionumber, >+ library => $library->branchcode, >+ } >+ ); >+ >+ C4::Circulation::AddIssue( $patron, $item->barcode ); >+ >+ is( $biblio->items->filter_by_has_recalls->count, 0, "0 items with recalls on this record" ); >+ >+ Koha::Recalls->add_recall( { biblio => $item->biblio, item => $item, patron => $patron } ); >+ >+ is( $biblio->items->filter_by_has_recalls->count, 1, "1 item with recalls on this record" ); >+ >+ $schema->storage->txn_rollback; >+ >+}; >+ >+subtest 'filter_by_available' => sub { >+ plan tests => 6; >+ >+ $schema->storage->txn_begin; >+ >+ my $library = $builder->build_object( { class => 'Koha::Libraries' } ); >+ my $biblio = $builder->build_sample_biblio(); >+ my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); >+ t::lib::Mocks::mock_userenv( { branchcode => $patron->branchcode } ); >+ >+ my $item_1 = $builder->build_sample_item( >+ { >+ biblionumber => $biblio->biblionumber, >+ library => $library->branchcode, >+ itemlost => 0, >+ withdrawn => 0, >+ damaged => 0, >+ notforloan => 0, >+ onloan => undef, >+ } >+ ); >+ >+ my $item_2 = $builder->build_sample_item( >+ { >+ biblionumber => $biblio->biblionumber, >+ library => $library->branchcode, >+ itemlost => 0, >+ withdrawn => 0, >+ damaged => 0, >+ notforloan => 0, >+ onloan => undef, >+ } >+ ); >+ >+ my $item_3 = $builder->build_sample_item( >+ { >+ biblionumber => $biblio->biblionumber, >+ library => $library->branchcode, >+ itemlost => 0, >+ withdrawn => 0, >+ damaged => 0, >+ notforloan => 0, >+ onloan => undef, >+ } >+ ); >+ >+ my $item_4 = $builder->build_sample_item( >+ { >+ biblionumber => $biblio->biblionumber, >+ library => $library->branchcode, >+ itemlost => 0, >+ withdrawn => 0, >+ damaged => 0, >+ notforloan => 0, >+ onloan => undef, >+ } >+ ); >+ >+ my $item_5 = $builder->build_sample_item( >+ { >+ biblionumber => $biblio->biblionumber, >+ library => $library->branchcode, >+ itemlost => 0, >+ withdrawn => 0, >+ damaged => 0, >+ notforloan => 0, >+ onloan => undef, >+ } >+ ); >+ >+ # Create items with varying states >+ # Test: Initial available items >+ is( >+ $biblio->items->filter_by_available->count, >+ 5, >+ "Filtered to 4 available items" >+ ); >+ >+ # Mark item_1 as lost >+ $item_1->itemlost(3)->store; >+ C4::Circulation::LostItem( $item_1->itemnumber, 1 ); >+ >+ is( >+ $biblio->items->filter_by_available->count, >+ 4, >+ "Filtered to 4 available items, 1 is lost" >+ ); >+ >+ #Mark item_2 as damaged >+ $item_2->damaged(1)->store; >+ >+ is( >+ $biblio->items->filter_by_available->count, >+ 3, >+ "Filtered to 3 available items, 1 is lost, 1 is damaged" >+ ); >+ >+ #Mark item_3 as withdrawn >+ $item_3->withdrawn(1)->store; >+ >+ is( >+ $biblio->items->filter_by_available->count, >+ 2, >+ "Filtered to 2 available items, 1 is lost, 1 is damaged, 1 is withdrawn" >+ ); >+ >+ #Checkout item_4 >+ C4::Circulation::AddIssue( $patron, $item_4->barcode ); >+ is( >+ $biblio->items->filter_by_available->count, >+ 1, >+ "Filtered to 1 available items, 1 is lost, 1 is damaged, 1 is withdrawn, 1 is checked out" >+ ); >+ >+ #Mark item_5 as notforloan >+ $item_5->notforloan(1)->store; >+ is( >+ $biblio->items->filter_by_available->count, >+ 0, >+ "Filtered to 0 available items, 1 is lost, 1 is damaged, 1 is withdrawn, 1 is checked out, 1 is notforloan" >+ ); >+ >+ $schema->storage->txn_rollback; >+}; >-- >2.43.0
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 37334
:
172300
|
172301
|
172302
|
173499
|
173500
|
173501
|
173949
|
173950
|
173951
|
173982
|
174310
|
174798
|
174799
|
174800
|
174801
|
174802
|
174803
|
174804
|
174813
|
174814
|
174901
|
174902
|
174903
|
174955
|
175859
|
175860
|
175861
|
175862
|
175863
|
175864
|
175865
|
175866
|
175867
|
175868
|
175869
|
175870
|
175871
|
175999
|
176000
|
176001
|
176002
|
176003
|
176004
|
176005
|
176006
|
176007
|
176008
|
176009
|
176010
|
176011
|
176023
|
176024
|
176025
|
176026
|
176027
|
176028
|
176029
|
176030
|
176031
|
176032
|
176033
|
176034
|
176035
|
176475
|
176477
|
176480
|
176481
|
177321
|
177415
|
177416
|
177417
|
177418
|
177419
|
177420
|
177421
|
177422
|
177423
|
177424
|
177425
|
177426
|
177427
|
177428
|
177429
|
177430
|
177431
|
177432
|
177466
|
177516
|
177520
|
177524
|
177525
|
177526
|
177527
|
177528
|
177529
|
177530
|
177531
|
177764
|
177765
|
177766
|
177767
|
177768
|
177769
|
177770
|
177771
|
177772
|
177773
|
177774
|
177775
|
177776
|
177777
|
177778
|
177779
|
177780
|
177781
|
177782
|
177783
|
178620
|
178621
|
178622
|
178623
|
178624
|
178625
|
178626
|
178627
|
178628
|
178629
|
178630
|
178631
|
178632
|
178633
|
178634
|
178635
|
178636
|
178637
|
180064
|
180157
|
180158
|
180159
|
180160
|
180161
|
180162
|
180163
|
180164
|
180165
|
180166
|
180167
|
180168
|
180169
|
180170
|
180171
|
180172
|
180173
|
180174
|
180175
|
180196
|
181161
|
181162
|
181163
|
181164
|
181165
|
181166
|
181167
|
181169
|
181170
|
181171
|
181172
|
181173
|
181174
|
181175
|
181176
|
181177
|
181178
|
181179
|
181180
|
181181
|
181632
|
182359
|
182360
|
182361
|
182362
|
182363
|
182364
|
182365
|
182366
|
182367
|
182368
|
182369
|
182370
|
182371
|
182372
|
182373
|
182374
|
182375
|
182376
|
182552
|
182665
|
182733
|
184925
| 184974