From fa2b01021a3389b9cbe2b6d165a3a77995803450 Mon Sep 17 00:00:00 2001 From: Thibaud Guillot Date: Fri, 23 Feb 2024 10:14:40 +0100 Subject: [PATCH] Bug 8137: Add 'global checkout limits' table The aim here is to avoid the accumulation of checkouts possibilities through the rules of the various branches, in order to have a global quota. Test plan: 1) Apply this patch 2) Run updatedatabase script and rebuild schema, restart_all.. 3) On circulation rules page you will find a new table "Global checkout limits". 4) If you enter values, they will overwrite the existing values, creating limits for all branches combined. 5) Test it by set a "current checkouts allowed" to 2 on "standard rules section" and set a "global" maxissueqty to 1. Normally 1 is the new limit for now. 6) You can also add a "current checkouts allowed" on other branches and see that you can't accumulate checkouts (you can also delete "global" values and see that if you have 3 on a branch and 2 on other, you can perform 5 checkouts in total. 7) Note that on specific branch circulation rules page the table cannot be modified, but just shown as a reminder. If main matrix is empty on specific branch the table is hidden by default (cause if none values it's referred to standard rules). Sponsored-by: BibLibre Signed-off-by: Lisette Scheer --- Koha/CirculationRules.pm | 29 ++- Koha/Schema/Result/CirculationRule.pm | 11 +- Koha/Template/Plugin/CirculationRules.pm | 2 + admin/smart-rules.pl | 40 +++- .../Bug_8137-add-column-has_priority.pl | 15 ++ installer/data/mysql/kohastructure.sql | 1 + .../prog/en/modules/admin/smart-rules.tt | 193 +++++++++++++++++- 7 files changed, 276 insertions(+), 15 deletions(-) create mode 100755 installer/data/mysql/atomicupdate/Bug_8137-add-column-has_priority.pl diff --git a/Koha/CirculationRules.pm b/Koha/CirculationRules.pm index b0b2bd00c2b..d3c5a40caed 100644 --- a/Koha/CirculationRules.pm +++ b/Koha/CirculationRules.pm @@ -227,7 +227,9 @@ our $RULE_KINDS = { bookings_trail_period => { scope => [ 'branchcode', 'itemtype' ], }, - + has_priority => { + scope => [ 'branchcode', 'categorycode' ], + }, # Not included (deprecated?): # * accountsent # * reservecharge @@ -260,6 +262,7 @@ sub get_effective_rule { $params->{categorycode} //= undef; $params->{branchcode} //= undef; $params->{itemtype} //= undef; + $params->{has_priority} //= undef; my $rule_name = $params->{rule_name}; my $categorycode = $params->{categorycode}; @@ -290,7 +293,17 @@ sub get_effective_rule { } )->single; - return $rule; + $search_params->{has_priority} = 1; + + my $priority_rule = $self->search( + $search_params, + { + order_by => $order_by, + rows => 1, + } + )->single; + + return defined $priority_rule ? $priority_rule : $rule; } =head3 get_effective_rule_value @@ -318,10 +331,11 @@ sub get_effective_rule_value { my $categorycode = $params->{categorycode}; my $itemtype = $params->{itemtype}; my $branchcode = $params->{branchcode}; + my $has_priority = $params->{has_priority}; my $memory_cache = Koha::Cache::Memory::Lite->get_instance; - my $cache_key = sprintf "CircRules:%s:%s:%s:%s", $rule_name // q{}, - $categorycode // q{}, $branchcode // q{}, $itemtype // q{}; + my $cache_key = sprintf "CircRules:%s:%s:%s:%s:%s", $rule_name // q{}, + $categorycode // q{}, $branchcode // q{}, $itemtype // q{} ,$has_priority // q{}; my $cached = $memory_cache->get_from_cache($cache_key); return $cached if $cached; @@ -344,6 +358,7 @@ sub get_effective_rules { my $categorycode = $params->{categorycode}; my $itemtype = $params->{itemtype}; my $branchcode = $params->{branchcode}; + my $has_priority = $params->{has_priority}; my $r; foreach my $rule (@$rules) { @@ -353,6 +368,7 @@ sub get_effective_rules { categorycode => $categorycode, itemtype => $itemtype, branchcode => $branchcode, + has_priority => $has_priority, } ); @@ -394,6 +410,8 @@ sub set_rule { my $itemtype = $params->{itemtype}; my $rule_name = $params->{rule_name}; my $rule_value = $params->{rule_value}; + my $has_priority = $params->{has_priority}; + my $can_be_blank = defined $kind_info->{can_be_blank} ? $kind_info->{can_be_blank} : 1; $rule_value = undef if defined $rule_value && $rule_value eq "" && !$can_be_blank; my $is_monetary = defined $kind_info->{is_monetary} ? $kind_info->{is_monetary} : 0; @@ -409,6 +427,7 @@ sub set_rule { branchcode => $branchcode, categorycode => $categorycode, itemtype => $itemtype, + has_priority => $has_priority } )->next(); @@ -429,6 +448,7 @@ sub set_rule { itemtype => $itemtype, rule_name => $rule_name, rule_value => $rule_value, + has_priority => $has_priority, } ); $rule->store(); @@ -458,6 +478,7 @@ sub set_rules { $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode}; $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode}; $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype}; + $set_params{has_priority} = $params->{has_priority} if exists $params->{has_priority}; my $rules = $params->{rules}; my $rule_objects = []; diff --git a/Koha/Schema/Result/CirculationRule.pm b/Koha/Schema/Result/CirculationRule.pm index 50dc93c5ad5..9cce1ba39ef 100644 --- a/Koha/Schema/Result/CirculationRule.pm +++ b/Koha/Schema/Result/CirculationRule.pm @@ -62,6 +62,11 @@ __PACKAGE__->table("circulation_rules"); is_nullable: 0 size: 32 +=head2 has_priority + + data_type: 'tinyint' + is_nullable: 1 + =cut __PACKAGE__->add_columns( @@ -77,6 +82,8 @@ __PACKAGE__->add_columns( { data_type => "varchar", is_nullable => 0, size => 32 }, "rule_value", { data_type => "varchar", is_nullable => 0, size => 32 }, + "has_priority", + { data_type => "tinyint", is_nullable => 1 }, ); =head1 PRIMARY KEY @@ -177,8 +184,8 @@ __PACKAGE__->belongs_to( ); -# Created by DBIx::Class::Schema::Loader v0.07042 @ 2018-10-05 14:29:17 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:QHMqvrtX0ohJe70PHUYZ0Q +# Created by DBIx::Class::Schema::Loader v0.07051 @ 2024-12-20 12:51:01 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:P5S9o9p6aJ38EOkLQQOW0Q =head2 koha_objects_class diff --git a/Koha/Template/Plugin/CirculationRules.pm b/Koha/Template/Plugin/CirculationRules.pm index d19c27b543b..1e1de6ba22a 100644 --- a/Koha/Template/Plugin/CirculationRules.pm +++ b/Koha/Template/Plugin/CirculationRules.pm @@ -76,6 +76,7 @@ sub Search { $branchcode = undef if $branchcode eq q{} or $branchcode eq q{*}; $categorycode = undef if $categorycode eq q{} or $categorycode eq q{*}; $itemtype = undef if $itemtype eq q{} or $itemtype eq q{*}; + my $has_priority = $params->{has_priority} // undef; my $rule = Koha::CirculationRules->search( { @@ -83,6 +84,7 @@ sub Search { categorycode => $categorycode, itemtype => $itemtype, rule_name => $rule_name, + has_priority => $has_priority } )->next; diff --git a/admin/smart-rules.pl b/admin/smart-rules.pl index 7f5448f511c..37eeae44f66 100755 --- a/admin/smart-rules.pl +++ b/admin/smart-rules.pl @@ -755,6 +755,44 @@ elsif ( $op eq 'cud-add' ) { rules => { waiting_hold_cancellation => undef }, } ); +} elsif ( $op eq 'cud-set-global-checkout-limits' ) { + + my $categorycode = $input->param('categorycode'); + my $itemtype = $input->param('itemtype'); + my $maxissueqty = scalar $input->param('maxissueqty'); + my $maxonsiteissueqty = scalar $input->param('maxonsiteissueqty'); + + my %checkout_rules; + $checkout_rules{'maxissueqty'} = strip_non_numeric($maxissueqty); + $checkout_rules{'maxonsiteissueqty'} = strip_non_numeric($maxonsiteissueqty); + + while (my ($rule_name, $rule_value) = each(%checkout_rules)) { + if(defined $rule_value && $rule_value ne '') { + Koha::CirculationRules->set_rule( + { categorycode => ( $categorycode eq '*' ) ? undef : $categorycode, + itemtype => ( $itemtype eq '*' ) ? undef : $itemtype, + branchcode => undef, + has_priority => 1, + rule_name => $rule_name, + rule_value => $rule_value + }); + } + } +} elsif ( $op eq 'cud-delete-global-checkout-limits' ) { + my $categorycode = $input->param('categorycode'); + my $itemtype = $input->param('itemtype'); + + Koha::CirculationRules->set_rules( + { categorycode => ( $categorycode eq '*' ) ? undef : $categorycode, + itemtype => ( $itemtype eq '*' ) ? undef : $itemtype, + branchcode => undef, + has_priority => 1, + rules => { + maxissueqty => undef, + maxonsiteissueqty => undef, + }, + } + ); } my $refundLostItemFeeRule = @@ -783,7 +821,7 @@ my @used_itemtypes = Koha::CirculationRules->search( { branchcode => $humanbranch }, { columns => ['itemtype'], distinct => 1, } ) ->get_column('itemtype'); -my $all_rules = Koha::CirculationRules->search( { branchcode => $humanbranch } ); +my $all_rules = Koha::CirculationRules->search({ branchcode => $humanbranch, has_priority => undef }); my $definedbranch = $all_rules->count ? 1 : 0; my $rules = {}; diff --git a/installer/data/mysql/atomicupdate/Bug_8137-add-column-has_priority.pl b/installer/data/mysql/atomicupdate/Bug_8137-add-column-has_priority.pl new file mode 100755 index 00000000000..20742a43de1 --- /dev/null +++ b/installer/data/mysql/atomicupdate/Bug_8137-add-column-has_priority.pl @@ -0,0 +1,15 @@ +use Modern::Perl; + +return { + bug_number => "8137", + description => "Add column has_priority", + up => sub { + my ($args) = @_; + my ( $dbh, $out ) = @$args{qw(dbh out)}; + + if ( !column_exists( 'circulation_rules', 'has_priority' ) ) { + $dbh->do('ALTER TABLE circulation_rules ADD IF NOT EXISTS has_priority TINYINT(1) DEFAULT NULL'); + say $out "Added column 'circulation_rules.has_priority'"; + } + }, +}; diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index ce39d6a933f..4367412a686 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -1881,6 +1881,7 @@ CREATE TABLE `circulation_rules` ( `itemtype` varchar(10) DEFAULT NULL, `rule_name` varchar(32) NOT NULL, `rule_value` varchar(32) NOT NULL, + `has_priority` tinyint(1) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `branchcode` (`branchcode`,`categorycode`,`itemtype`,`rule_name`), KEY `circ_rules_ibfk_2` (`categorycode`), diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smart-rules.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smart-rules.tt index a1b9d79a41e..be7150f36f4 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smart-rules.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smart-rules.tt @@ -83,6 +83,7 @@ [% Koha.Preference('CircControl') | html %] and the HomeOrHoldingBranch system preference which is set to [% Koha.Preference('HomeOrHoldingBranch') | html %].

+

The “Global checkout limits” section is designed to create a limit for all librairies, thus preventing checkouts accumulation.

@@ -890,6 +891,108 @@
[% END %] +
+

Global checkout limits

+

Specify checkout limits for all librairies.

+
+ [% INCLUDE 'csrf-token.inc' %] + + + + + + + + + + + + + [% FOREACH c IN categorycodes %] + [% SET c = '' UNLESS c.defined %] + [% FOREACH i IN itemtypes %] + [% SET i = '' UNLESS i.defined %] + [% SET maxissueqty = CirculationRules.Search( '', c, i, 'maxissueqty', { want_rule => 1, has_priority => 1 } ) %] + [% SET maxonsiteissueqty = CirculationRules.Search( '', c, i, 'maxonsiteissueqty', { want_rule => 1, has_priority => 1 } ) %] + [% SET show_rule = maxissueqty || maxonsiteissueqty %] + [% IF show_rule %] + + + + + + + + [% END %] + [% END %] + [% END %] + + + + + + + + +
Patron categoryItem typeActionsCurrent checkouts allowedCurrent on-site checkouts allowed
+ [% IF c == undef %] + All + [% ELSE %] + [% Categories.GetName(c) | html %] + [% END %] + + [% IF i == undef %] + All + [% ELSE %] + [% ItemTypes.GetDescription(i,1) | html %] + [% END %] + + Edit + Delete + + [% IF maxissueqty.rule_value && maxissueqty.rule_value != '' %] + [% maxissueqty.rule_value | html %] + [% ELSE %] + Not defined + [% END %] + + [% IF maxonsiteissueqty.rule_value && maxonsiteissueqty.rule_value != '' %] + [% maxonsiteissueqty.rule_value | html %] + [% ELSE %] + Not defined + [% END %] +
+ + + + + + +
+
+
+
[% IF humanbranch %]

Waiting hold cancellation policy for [% Branches.GetName( humanbranch ) | html %]

@@ -1459,7 +1562,7 @@ }); }); - function clear_edit() { + function clear_edit_form(editRowSelector, tableSelector) { var cancel = confirm(_("Are you sure you want to cancel your changes?")); if (!cancel) return; $("#default-circulation-rules tr").removeClass("highlighted-row"); @@ -1472,18 +1575,66 @@ $(this).val(""); $(this).prop("disabled", false); } - if (type == "checkbox") { + if (type === "checkbox") { $(this).prop("checked", false); } }); - $(edit_row).find("select").prop("disabled", false); - $(edit_row).find("select option:selected").removeAttr("selected"); - $(edit_row).find("select option:first-child").attr("selected", "selected"); - $(edit_row).find("td:last input[name='clear']").remove(); + edit_row.find("select").prop('disabled', false); + edit_row.find("select option:selected").prop('selected', false); + edit_row.find("select option:first-child").prop("selected", true); + edit_row.find("td:last input[name='clear']").remove(); } var MSG_CONFIRM_DELETE = _("Are you sure you want to delete this rule? This cannot be undone."); - + $(".editrule-global-checkout-limits").click(function(){ + if ( $("#global_checkout_limits_edit_row").find("input[type='text']").filter(function(){return this.value.length > 0 }).length > 0 ) { + var edit = confirm(_("Are you sure you want to edit another rule?")); + if (!edit) return false; + } + $('#global-checkout-limits-rules td').removeClass('highlighted-row'); + $(this).parent().parent().find("td").each(function (i) { + $(this).addClass('highlighted-row'); + itm_code = $(this).data('code'); + itm_text = $(this).text(); + itm_text = itm_text.replace(/^\s*|\s*$/g,''); + var current_column = $("#global_checkout_limits_edit_row td:eq("+i+")"); + var current_input_id = $(current_column).children('input').first().attr('id'); + $(current_column).find("input[type='text']").val(itm_text); + // select the corresponding option + $(current_column).find("select option").each(function(){ + // Reset selection status for all options + $(this).prop('selected', false); + // Declare opt here to ensure it is scoped correctly within the loop + let opt = $(this).val(); + // Select the matching option + if (opt == itm_code) { + $(this).prop('selected', true); + } + }); + // After setting the correct option, update the select to reflect the change + $(current_column).find('select').trigger('change'); + if ( i == 0 || i == 1 ) { + // Disable the 2 first columns, we cannot update them. + var val = $(current_column).find("select option:selected").val(); + var name = "categorycode"; + if ( i == 1 ) { + name="itemtype"; + } + // Remove potential previous input added + $(current_column).find("input").remove(); + $(current_column).append(""); + } else if ( current_input_id === "global_checkout_limits_maxissueqty" || current_input_id === "global_checkout_limits_maxonsiteissueqty" ) { + // If the value is not an integer for + // - "Global current checkouts allowed" + // - "Global current on-site checkouts allowed" + // The value will be "Not defined" + if( !((parseFloat(itm_text) == parseInt(itm_text)) && !isNaN(itm_text)) ) { + $(current_column).find("input[type='text']").val(""); + } + } + }); + return false; + }); $(document).ready(function () { $('[data-bs-toggle="popover"]').popover(); @@ -1632,8 +1783,12 @@ }); $(".clear_edit").on("click", function (e) { e.preventDefault(); - clear_edit(); + clear_edit_form("#edit_row", "#default-circulation-rules"); }); + $(".clear_edit_global_checkouts_limit").on("click",function(e){ + e.preventDefault(); + clear_edit_form("#global_checkout_limits_edit_row", "#global-checkout-limits-rules"); + }); $("#set-article-requests-daily-limit").on("submit", function () { if (!$("input[name='open_article_requests_limit'").val().length) { @@ -1724,6 +1879,28 @@ f.find("[name='branch']").val($(this).data("branch")); return f.submit(); }); + + $(".delete-global-checkout-limits").on("click", function(e){ + e.preventDefault(); + if ( !confirmDelete(MSG_CONFIRM_DELETE) ) { + return false; + } + let f = $("#delete_form"); + f.find("[name='op']").val('cud-delete-global-checkout-limits'); + f.find("[name='itemtype']").val($(this).data('itemtype')); + f.find("[name='categorycode']").val($(this).data('categorycode')); + f.find("[name='branch']").val($(this).data('branch')); + return f.submit(); + }); + + if($("#branch").val() != '*') { + var rowCount = $("#default-circulation-rules tbody tr").length; + if(rowCount == 1){ + $("#global_checkout_limits").remove(); + } else { + $("#global_checkout_limits .restricted-access").remove(); + } + } }); [% END %] -- 2.39.5