From b66b8d2c54cc9438df0365bc4165f3c07e43519a Mon Sep 17 00:00:00 2001 From: Paul Derscheid Date: Tue, 24 Feb 2026 15:07:43 +0100 Subject: [PATCH] Bug 41917: Add parent itemtype fallback to CirculationRules::get_effective_rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - When only a global rule is found, check for a parent itemtype-specific rule - Two-query strategy preserves existing priority ordering To test: - Run t/db_dependent/Koha/CirculationRules.t - Verify child itemtype falls back to parent-specific rule over the global default - Verify child-specific rules take priority over parent rules - Verify orphan itemtypes still fall back to global - Verify branch-specific parent rules take priority Sponsored-by: Büchereizentrale Schleswig-Holstein --- Koha/CirculationRules.pm | 24 ++++++ t/db_dependent/Koha/CirculationRules.t | 115 ++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/Koha/CirculationRules.pm b/Koha/CirculationRules.pm index dc599d5f7df..37df584ceb6 100644 --- a/Koha/CirculationRules.pm +++ b/Koha/CirculationRules.pm @@ -25,6 +25,7 @@ use Koha::Exceptions::CirculationRule; use Koha::CirculationRule; use Koha::Caches; use Koha::Cache::Memory::Lite; +use Koha::ItemTypes; use Koha::Number::Price; use base qw(Koha::Objects); @@ -294,6 +295,29 @@ sub get_effective_rule { } )->single; + if ( !defined $itemtype || ( $rule && defined $rule->itemtype ) ) { + return $rule; + } + + my $itemtype_obj = Koha::ItemTypes->find($itemtype); + return $rule unless $itemtype_obj; + + my $parent = $itemtype_obj->parent; + return $rule unless $parent; + + my $parent_search = {%$search_params}; + $parent_search->{itemtype} = [ $parent->itemtype, undef ]; + + my $parent_rule = $self->search( + $parent_search, + { + order_by => $order_by, + rows => 1, + } + )->single; + + return $parent_rule if $parent_rule && defined $parent_rule->itemtype; + return $rule; } diff --git a/t/db_dependent/Koha/CirculationRules.t b/t/db_dependent/Koha/CirculationRules.t index bde48da84bc..e5cdf6b73e5 100755 --- a/t/db_dependent/Koha/CirculationRules.t +++ b/t/db_dependent/Koha/CirculationRules.t @@ -21,7 +21,7 @@ use Modern::Perl; use Benchmark; use Test::NoWarnings; -use Test::More tests => 9; +use Test::More tests => 10; use Test::Deep qw( cmp_methods ); use Test::Exception; @@ -1149,6 +1149,119 @@ subtest 'get_lostreturn_policy() tests' => sub { $schema->storage->txn_rollback; }; +subtest 'get_effective_rule parent itemtype fallback' => sub { + plan tests => 6; + + $schema->storage->txn_begin; + + my $branch = $builder->build( { source => 'Branch' } )->{branchcode}; + my $branch2 = $builder->build( { source => 'Branch' } )->{branchcode}; + my $category = $builder->build( { source => 'Category' } )->{categorycode}; + + my $parent_itype = $builder->build_object( { class => 'Koha::ItemTypes', value => { parent_type => undef } } ); + my $child_itype = + $builder->build_object( { class => 'Koha::ItemTypes', value => { parent_type => $parent_itype->itemtype } } ); + my $orphan_itype = $builder->build_object( { class => 'Koha::ItemTypes', value => { parent_type => undef } } ); + + Koha::CirculationRules->set_rule( + { + branchcode => undef, + categorycode => undef, + itemtype => undef, + rule_name => 'fine', + rule_value => '0.50', + } + ); + + Koha::CirculationRules->set_rule( + { + branchcode => undef, + categorycode => undef, + itemtype => $parent_itype->itemtype, + rule_name => 'fine', + rule_value => '1.00', + } + ); + + my $rule = Koha::CirculationRules->get_effective_rule( + { + rule_name => 'fine', + itemtype => $child_itype->itemtype, + } + ); + is( $rule->rule_value, '1.00', 'Child itemtype falls back to parent-specific rule over global' ); + + Koha::CirculationRules->set_rule( + { + branchcode => undef, + categorycode => undef, + itemtype => $child_itype->itemtype, + rule_name => 'fine', + rule_value => '2.00', + } + ); + + $rule = Koha::CirculationRules->get_effective_rule( + { + rule_name => 'fine', + itemtype => $child_itype->itemtype, + } + ); + is( $rule->rule_value, '2.00', 'Child-specific rule takes priority over parent' ); + + Koha::CirculationRules->search( + { + itemtype => $child_itype->itemtype, + rule_name => 'fine', + } + )->delete; + + $rule = Koha::CirculationRules->get_effective_rule( + { + rule_name => 'fine', + itemtype => $child_itype->itemtype, + } + ); + is( $rule->rule_value, '1.00', 'After deleting child rule, falls back to parent again' ); + + $rule = Koha::CirculationRules->get_effective_rule( + { + rule_name => 'fine', + itemtype => $orphan_itype->itemtype, + } + ); + is( $rule->rule_value, '0.50', 'Orphan itemtype (no parent) falls back to global' ); + + Koha::CirculationRules->set_rule( + { + branchcode => $branch, + categorycode => undef, + itemtype => $parent_itype->itemtype, + rule_name => 'fine', + rule_value => '3.00', + } + ); + + $rule = Koha::CirculationRules->get_effective_rule( + { + rule_name => 'fine', + itemtype => $child_itype->itemtype, + branchcode => $branch, + } + ); + is( $rule->rule_value, '3.00', 'Branch-specific parent rule takes priority' ); + + $rule = Koha::CirculationRules->get_effective_rule( + { + rule_name => 'fine', + itemtype => undef, + } + ); + is( $rule->rule_value, '0.50', 'Undef itemtype returns global (no parent lookup)' ); + + $schema->storage->txn_rollback; +}; + sub _is_row_match { my ( $rule, $expected, $message ) = @_; -- 2.39.5