From 9bbcc8135bad1606ef96fc1fb09607dda62050f6 Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Tue, 9 Dec 2025 17:20:17 +0000 Subject: [PATCH] Bug 40445: (follow-up) Support negative cashup amounts for float deficits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When cash refunds exceed takings in a session, the register's float is depleted, resulting in negative bankable amounts. This patch adds full support for this scenario. Backend changes: - Removed abs() from start_cashup to preserve sign of amounts - Updated validation to allow negative amounts (while preventing zero) - Changed expected_amount calculation to: total * -1 (consistent) Frontend changes (registers.tt): - Dynamic modal messages based on amount sign - Positive: "Remove £X cash from register to bank" - Negative: "Top up the register with £X to restore the float" - Updated input pattern to accept negative numbers: ^-?\d+(\.\d{2})?$ - Labels change based on context (add vs. remove) Frontend changes (register.tt): - Added Template Toolkit conditionals for negative amounts - Updated input pattern to allow negative numbers - Consistent labeling with registers page All user-facing strings use _() for proper translation support. Test plan for librarians: Setup - Create negative cashup scenario: 1. Start with a register at its float amount (e.g., £100) 2. Process a large cash refund that exceeds takings Example: £80 in takings, £150 cash refund = -£70 bankable Test "Record cashup" modal (Quick cashup): 3. Go to registers page (pos/registers.pl) 4. Click "Record cashup" for the register with negative balance 5. Verify the modal shows: - Quick cashup: "Top up the register with £70.00 to restore the float" - Float reminder: "This will bring the register back to the expected float of £100.00" 6. Click "Quick cashup" 7. Enter -70.00 in the amount field (negative number) 8. Verify cashup completes successfully Test "Start cashup" modal (Two-phase): 9. Create another negative balance scenario 10. Click "Record cashup" 11. Verify Start cashup instructions show: - "Count cash in the register" - "The register can continue operating during counting" - "Complete the cashup by adding cash to restore the float" 12. Click "Start cashup" 13. Click "Complete cashup" for the register 14. Verify modal shows: - "Expected amount to add: £70.00" - Label: "Actual amount added to register:" 15. Enter -70.00 (the negative amount you're adding) 16. Verify cashup completes successfully Test reconciliation with negative amounts: 17. Start cashup on register with -£70.00 expected 18. Complete cashup with -£68.00 actual (£2 less added than expected) 19. Verify reconciliation shows correct deficit/surplus calculation 20. Add a reconciliation note and complete Test positive amounts still work (regression): 21. Create a normal positive cashup scenario 22. Verify all original messages appear correctly 23. Complete cashup with positive amount 24. Verify everything works as before Test individual register page consistency: 25. Navigate to individual register page (pos/register.pl) 26. Verify negative amount handling matches registers page 27. Test both modals show consistent messaging Expected results: - Negative amounts are accepted in all cashup workflows - Messages clearly indicate adding money vs. removing money - Reconciliation calculations work correctly for negative amounts - All translations display properly - Positive amount workflows unchanged --- Koha/Cash/Register.pm | 10 +- .../prog/en/modules/pos/register.tt | 94 ++++++++++++++++--- .../prog/en/modules/pos/registers.tt | 93 ++++++++++++++---- 3 files changed, 163 insertions(+), 34 deletions(-) diff --git a/Koha/Cash/Register.pm b/Koha/Cash/Register.pm index 15250aac94b..ba2a9e5aa5c 100644 --- a/Koha/Cash/Register.pm +++ b/Koha/Cash/Register.pm @@ -289,10 +289,10 @@ sub start_cashup { Koha::Exceptions::Object::DuplicateID->throw( error => "A cashup is already in progress for this register" ); } - my $expected_amount = abs( $self->outstanding_accountlines->total( { payment_type => [ 'CASH', 'SIP00' ] } ) ); + my $expected_amount = $self->outstanding_accountlines->total( { payment_type => [ 'CASH', 'SIP00' ] } ) * -1; # Prevent starting a cashup when there are no cash transactions - unless ( $expected_amount > 0 ) { + unless ( $expected_amount != 0 ) { Koha::Exceptions::Object::BadValue->throw( error => "Cannot start cashup with zero cash transactions", type => 'amount', @@ -345,10 +345,10 @@ sub add_cashup { } my $manager_id = $params->{manager_id}; - # Validate amount should always be a positive value + # Validate amount is a non-zero number my $amount = $params->{amount}; - unless ( looks_like_number($amount) && $amount > 0 ) { - Koha::Exceptions::Account::AmountNotPositive->throw( error => 'Cashup amount passed is not positive number' ); + unless ( looks_like_number($amount) && $amount != 0 ) { + Koha::Exceptions::Account::AmountNotPositive->throw( error => 'Cashup amount must be a non-zero number' ); } # Sanitize reconciliation note - treat empty/whitespace-only as undef diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/pos/register.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/pos/register.tt index be22824bad9..70ff0ae0133 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/pos/register.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/pos/register.tt @@ -447,7 +447,11 @@
  • [% IF cashup_in_progress %] - Expected cashup amount: + [% IF cashup_in_progress.amount < 0 %] + Expected amount to add: + [% ELSE %] + Expected cashup amount: + [% END %] [% ELSE %] Expected amount to remove: [% END %] @@ -457,12 +461,16 @@
  • - + Required
  • ' + _("Count cash in the register") + '
  • ' + + '
  • ' + _("The register can continue operating during counting") + '
  • ' + + '
  • ' + _("Complete the cashup by adding cash to restore the float") + '
  • ' + ); + } else { + $("#start_cashup_instructions_individual").html( + '
  • ' + _("Remove cash from the register for counting") + '
  • ' + + '
  • ' + _("The register can continue operating during counting") + '
  • ' + + '
  • ' + _("Complete the cashup once counted") + '
  • ' + ); + } + + // Update Quick cashup instructions + if (isNegative) { + $("#quick_cashup_instructions_individual").html( + '
  • ' + _("Top up the register with %s to restore the float").format(formattedAmount) + '
  • ' + ); + } else { + $("#quick_cashup_instructions_individual").html( + '
  • ' + _("Confirm you have removed %s cash from the register to bank immediately").format(formattedAmount) + '
  • ' + ); + } + + // Update float reminder + if (isNegative) { + $("#float_reminder_text_individual").html( + _("This will bring the register back to the expected float of %s").format(floatAmount) + ); + } else { + $("#float_reminder_text_individual").html( + _("Remember to leave the float amount of %s in the register.").format(floatAmount) + ); + } + }); + + // Handle Quick cashup button click for individual register + $("#quick_cashup_btn_individual").on("click", function(e){ + e.preventDefault(); + var form = $(this).closest('form'); + var bankableAmount = $("#register_bankable_amount").val(); + + // Change operation to cud-cashup (quick cashup) + form.find('input[name="op"]').val('cud-cashup'); + + // Set the amount to the expected bankable amount + form.find('input[name="amount"]').val(bankableAmount); + + // Submit the form + form.submit(); + }); [% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/pos/registers.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/pos/registers.tt index 2f74d397eff..e983c8ed7c2 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/pos/registers.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/pos/registers.tt @@ -272,18 +272,16 @@