Bug 23415: Add debt override support to circulation checkout page

This patch implements fine override functionality for the checkouts
table on the circulation page (circ/circulation.pl), completing the
debt override feature for all renewal workflows.

Changes:
- Added override_debt checkbox to checkouts-table.inc when
  AllowFineOverrideRenewing preference is enabled
- Updated /svc/renew to accept and process override_debt parameter,
  matching the pattern used for override_limit
- Enhanced circulation-api-client.js to pass override_debt parameter
  through the renewal API call
- Implemented specific CSS classes for renewal error types
  (renewals-allowed-too_many, renewals-allowed-too_much_owing, etc.)
  to enable selective checkbox control
- Modified override checkbox handlers to selectively enable only
  relevant renewal checkboxes based on the specific blocking error:
  * override_limit enables checkboxes for 'too_many' and 'on_reserve'
  * override_debt enables checkboxes for 'too_much_owing' and
    'auto_too_much_owing'
- Added AllowFineOverrideRenewing JavaScript variable to circulation.tt

This provides clear visual feedback to staff about which renewals will
be affected by each override option, improving usability and reducing
confusion when multiple items have different blocking reasons.

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
This commit is contained in:
Martin Renvoize 2025-10-30 14:56:34 +00:00
parent 4b88b1de3d
commit 8b35dfcc9d
No known key found for this signature in database
GPG Key ID: 2D86E0EAEC83711B
8 changed files with 70 additions and 29 deletions

View File

@ -53,7 +53,7 @@ my $hard_due_date = $cgi->param('hard_due_date');
my ( $item, $checkout, $patron );
my $error = q{};
my ( $soonest_renew_date, $latest_auto_renew_date, $balance );
my ( $soonest_renew_date, $latest_auto_renew_date );
if ( $op eq 'cud-renew' && $barcode ) {
$barcode = barcodedecode($barcode) if $barcode;
@ -100,8 +100,6 @@ if ( $op eq 'cud-renew' && $barcode ) {
$can_renew = 1;
$error = undef;
} else {
$balance = $patron->account->balance;
$template->param( balance => $balance );
$can_renew = 0;
}
}

View File

@ -65,6 +65,10 @@
<label for="override_limit">Override renewal restrictions:</label>
<input type="checkbox" name="override_limit" id="override_limit" value="1" />
[% END %]
[% IF Koha.Preference( 'AllowFineOverrideRenewing' ) %]
<label for="override_debt">Override fine restrictions:</label>
<input type="checkbox" name="override_debt" id="override_debt" value="1" />
[% END %]
[% END %]
[% IF ( CAN_user_circulate_circulate_remaining_permissions ) %]
[% IF Koha.Preference( 'UnseenRenewals' ) %]
@ -76,6 +80,12 @@
<button class="btn btn-default" id="RenewAll"><i class="fa fa-book"></i> Renew all</button>
[% END %]
</fieldset>
[% IF patron.account.non_issues_charges >= Koha.Preference('FineNoRenewals') %]
<p
><strong>Note: </strong> Patron has <strong><span id="patron-non-issues-charges">[% patron.account.non_issues_charges | $Price %]</span></strong> of non-issuing charges outstanding, renewals are restricted at
<strong><span id="fine-no-renewals-limit">[% Koha.Preference('FineNoRenewals') | $Price %]</span></strong></p
>
[% END %]
[% IF Koha.Preference('ExportCircHistory') %]
<fieldset>

View File

@ -1096,6 +1096,7 @@
var exports_enabled = [% Koha.Preference('ExportCircHistory') ? 1 : 0 | html %];
var AllowRenewalLimitOverride = [% (CAN_user_circulate_override_renewals && Koha.Preference('AllowRenewalLimitOverride') )? 1: 0 | html %];
var AllowRenewalOnHoldOverride = [% (CAN_user_circulate_override_renewals && Koha.Preference('AllowRenewalOnHoldOverride') )? 1: 0 | html %];
var AllowFineOverrideRenewing = [% (CAN_user_circulate_override_renewals && Koha.Preference('AllowFineOverrideRenewing') )? 1: 0 | html %];
var AllowCirculate = [% (CAN_user_circulate_circulate_remaining_permissions)? 1 : 0 | html %];
var script = "circulation";
var relatives_borrowernumbers = new Array();

View File

@ -129,11 +129,14 @@
[% ELSIF error == "auto_renew" or error == "auto_too_much_owing" %]
<p>[% INCLUDE 'biblio-title.inc' biblio=item.biblio link = 1 %] ( [% item.barcode | html %] ) has been scheduled for automatic renewal. </p>
[% IF error == "auto_too_much_owing" %]
[% IF balance %]
<ul>
<li>The patron has a debt of [% balance | $Price %] and renewals are restricted at [% Koha.Preference('FineNoRenewals') | $Price %].</li>
</ul>
[% END %]
[% SET patron_account = borrower.account %]
[% SET total_balance = patron_account.balance %]
[% SET non_issues_charges = patron_account.non_issues_charges %]
[% SET fine_no_renewals = Koha.Preference('FineNoRenewals') %]
<ul>
<li>The patron has a total debt of [% total_balance | $Price %], of which [% non_issues_charges | $Price %] are non-issuing charges.</li>
<li>Renewals are blocked when non-issuing charges exceed [% fine_no_renewals | $Price %].</li>
</ul>
[% IF Koha.Preference('AllowFineOverrideRenewing') %]
<form method="post" action="/cgi-bin/koha/circ/renew.pl">
[% INCLUDE 'csrf-token.inc' %]
@ -156,8 +159,13 @@
[% END %]
[% END %]
[% ELSIF error == "too_much_owing" %]
[% SET patron_account = borrower.account %]
[% SET total_balance = patron_account.balance %]
[% SET non_issues_charges = patron_account.non_issues_charges %]
[% SET fine_no_renewals = Koha.Preference('FineNoRenewals') %]
<ul>
<li>The patron has a debt of [% balance | $Price %] and renewals are restricted at [% Koha.Preference('FineNoRenewals') | $Price %].</li>
<li>The patron has a total debt of [% total_balance | $Price %], of which [% non_issues_charges | $Price %] are non-issuing charges.</li>
<li>Renewals are blocked when non-issuing charges exceed [% fine_no_renewals | $Price %].</li>
</ul>
[% IF Koha.Preference('AllowFineOverrideRenewing') %]
<form method="post" action="/cgi-bin/koha/circ/renew.pl">

View File

@ -424,7 +424,7 @@ function LoadIssuesTable() {
"</span>";
span_style = "display: none";
span_class = "renewals-allowed";
span_class = "renewals-allowed-too_many";
} else if (oObj.can_renew_error == "too_unseen") {
msg +=
"<span>" +
@ -478,7 +478,7 @@ function LoadIssuesTable() {
"</span>";
span_style = "display: none";
span_class = "renewals-allowed";
span_class = "renewals-allowed-auto_too_much_owing";
} else if (
oObj.can_renew_error == "auto_account_expired"
) {
@ -507,13 +507,12 @@ function LoadIssuesTable() {
msg +=
"<span class='renewals-disabled'>" +
__(
"Cannot renew, the patron has a debt of " +
parseFloat(oObj.fine).format_price()
"Cannot renew - patron has debts blocking renewal"
) +
"</span>";
span_style = "display: none";
span_class = "renewals-allowed";
span_class = "renewals-allowed-too_much_owing";
} else {
msg +=
"<span class='renewals-disabled'>" +
@ -1010,6 +1009,7 @@ $(document).ready(function () {
function renew(item_id) {
var override_limit = $("#override_limit").is(":checked") ? 1 : 0;
var override_debt = $("#override_debt").is(":checked") ? 1 : 0;
$(this)
.parent()
@ -1029,6 +1029,7 @@ $(document).ready(function () {
patron_id: borrowernumber,
library_id: branchcode,
override_limit: override_limit,
override_debt: override_debt,
};
if (UnseenRenewals) {
@ -1420,16 +1421,28 @@ $(document).ready(function () {
.click(function () {
if (this.checked) {
if (AllowRenewalLimitOverride) {
$(".renewals-allowed").show();
$(".renewals-disabled").hide();
$(".renewals-allowed-too_many").show();
}
if (AllowRenewalOnHoldOverride) {
$(".renewals-allowed-on_reserve").show();
}
} else {
$(".renewals-allowed").hide();
$(".renewals-allowed-too_many").hide();
$(".renewals-allowed-on_reserve").hide();
$(".renewals-disabled").show();
}
})
.prop("checked", false);
}
if (AllowFineOverrideRenewing) {
$("#override_debt")
.click(function () {
if (this.checked) {
$(".renewals-allowed-too_much_owing").show();
$(".renewals-allowed-auto_too_much_owing").show();
} else {
$(".renewals-allowed-too_much_owing").hide();
$(".renewals-allowed-auto_too_much_owing").hide();
}
})
.prop("checked", false);

View File

@ -31,11 +31,12 @@ export class CirculationAPIClient {
this.httpClient.post({
endpoint: "renew",
body:
"itemnumber=%s&borrowernumber=%s&branchcode=%s&override_limit=%s".format(
"itemnumber=%s&borrowernumber=%s&branchcode=%s&override_limit=%s&override_debt=%s".format(
checkout.item_id,
checkout.patron_id,
checkout.library_id,
checkout.override_limit
checkout.override_limit,
checkout.override_debt || 0
) +
(checkout.seen !== undefined
? "&seen=%s".format(checkout.seen)

View File

@ -46,10 +46,12 @@ print $input->header( -type => 'application/json', -charset => 'UTF-8' );
my $itemnumber = $input->param('itemnumber');
my $borrowernumber = $input->param('borrowernumber');
my $override_limit = $input->param('override_limit');
my $override_debt = $input->param('override_debt');
my $branchcode = $input->param('branchcode')
|| C4::Context->userenv->{'branch'};
my $seen = $input->param('seen');
my $date_due;
if ( $input->param('date_due') ) {
$date_due = dt_from_string( scalar $input->param('date_due') );
}
@ -79,6 +81,13 @@ if ( $data->{error} && $data->{error} eq 'too_many' && C4::Context->preference('
push @{$confirmations}, 'RENEWAL_LIMIT' if $override_limit;
}
if ( $data->{error} && ( $data->{error} eq 'too_much_owing' or $data->{error} eq 'auto_too_much_owing' ) ) {
if ( $override_debt && C4::Context->preference('AllowFineOverrideRenewing') ) {
$data->{renew_okay} = 1;
$data->{error} = undef;
}
}
if ( $data->{renew_okay} || ( $seen && $data->{error} eq 'too_unseen' ) ) {
try {
$date_due = AddRenewal(

View File

@ -59,9 +59,11 @@ describe("Circulation - FineNoRenewals and AllowFineOverrideRenewing", () => {
// Should see error message about patron debt
cy.get(".dialog.alert").should("be.visible");
cy.get(".dialog.alert li").should(
cy.get(".dialog.alert").should("contain", "total debt of");
cy.get(".dialog.alert").should("contain", "non-issuing charges");
cy.get(".dialog.alert").should(
"contain",
`The patron has a debt of`
"Renewals are blocked when non-issuing charges exceed"
);
});
@ -80,11 +82,8 @@ describe("Circulation - FineNoRenewals and AllowFineOverrideRenewing", () => {
// Should see error message about patron debt
cy.get(".dialog.alert").should("be.visible");
cy.get(".dialog.alert li").should(
"contain",
`The patron has a debt of`
);
cy.get(".dialog.alert").should("contain", "total debt of");
cy.get(".dialog.alert").should("contain", "non-issuing charges");
// Should see override button
cy.get('.dialog.alert form button[type="submit"].approve').should(
@ -111,9 +110,11 @@ describe("Circulation - FineNoRenewals and AllowFineOverrideRenewing", () => {
// Should see error message about patron debt
cy.get(".dialog.alert").should("be.visible");
cy.get(".dialog.alert li").should(
cy.get(".dialog.alert").should("contain", "total debt of");
cy.get(".dialog.alert").should("contain", "non-issuing charges");
cy.get(".dialog.alert").should(
"contain",
`The patron has a debt of`
"Renewals are blocked when non-issuing charges exceed"
);
// Should NOT see override button