From a96ecdc466ada67ee596d04606faccfd0ae9c8fc Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Fri, 8 Aug 2025 13:19:45 +0100 Subject: [PATCH] Bug 40445: Add cashup reconciliation functionality to point of sale This patch implements comprehensive cashup reconciliation capabilities for the Koha point-of-sale system, allowing staff to record actual cash amounts and automatically track surplus or deficit discrepancies. Backend Changes: - Enhanced Koha::Cash::Register->add_cashup() method to accept actual_amount and optional notes - Automatic creation of CASHUP_SURPLUS or CASHUP_DEFICIT accountlines when discrepancies detected - Database transaction handling ensures atomicity of cashup actions and reconciliation records - Input validation and sanitization for amounts and notes Frontend Changes: - Interactive cashup modal requiring staff to enter actual amount removed from register - Real-time reconciliation calculation showing surplus/deficit as user types - Conditional note field (1000 char limit) appears only when discrepancies are detected - Enhanced cashup summary modal displays reconciliation details prominently - Empty amount field forces conscious entry (no pre-population) Features: - Balanced cashups: Only cashup action created (no reconciliation accountlines) - Surplus cashups: Creates CASHUP_SURPLUS credit with audit details - Deficit cashups: Creates CASHUP_DEFICIT debit with audit details - Staff notes: Optional explanations for discrepancies stored with system calculations - Full audit trail: All reconciliation data preserved with timestamps and manager links The implementation ensures mathematical balance is maintained while providing complete audit capabilities for cash register discrepancies. --- Koha/Cash/Register.pm | 106 ++++++++++++++++-- Koha/Cash/Register/Cashup.pm | 27 ++++- .../en/includes/modals/cashup_summary.inc | 28 ++++- .../prog/en/modules/pos/register.tt | 88 ++++++++++++++- .../intranet-tmpl/prog/js/cashup_modal.js | 88 ++++++++++++++- pos/register.pl | 33 ++++-- 6 files changed, 340 insertions(+), 30 deletions(-) diff --git a/Koha/Cash/Register.pm b/Koha/Cash/Register.pm index 19a1ecf9372..0ca8f714ad0 100644 --- a/Koha/Cash/Register.pm +++ b/Koha/Cash/Register.pm @@ -17,6 +17,7 @@ package Koha::Cash::Register; use Modern::Perl; +use Koha::Account; use Koha::Account::Lines; use Koha::Account::Offsets; use Koha::Cash::Register::Actions; @@ -208,27 +209,114 @@ sub drop_default { my $cashup = $cash_register->add_cashup( { - manager_id => $logged_in_user->id, - amount => $cash_register->outstanding_accountlines->total + manager_id => $logged_in_user->id, + amount => $amount_removed_from_register, + [ reconciliation_note => $reconciliation_note ] } ); Add a new cashup action to the till, returns the added action. +If amount differs from expected amount, creates surplus/deficit accountlines. =cut sub add_cashup { my ( $self, $params ) = @_; - my $rs = $self->_result->add_to_cash_register_actions( - { - code => 'CASHUP', - manager_id => $params->{manager_id}, - amount => $params->{amount} + my $manager_id = $params->{manager_id}; + my $amount = $params->{amount}; + my $reconciliation_note = $params->{reconciliation_note}; + + # Sanitize reconciliation note - treat empty/whitespace-only as undef + if ( defined $reconciliation_note ) { + $reconciliation_note = substr( $reconciliation_note, 0, 1000 ); # Limit length + $reconciliation_note =~ s/^\s+|\s+$//g; # Trim whitespace + $reconciliation_note = undef if $reconciliation_note eq ''; # Empty after trim = undef + } + + # Calculate expected amount from outstanding accountlines + my $expected_amount = $self->outstanding_accountlines->total; + + # For backward compatibility, if no actual amount is specified, use expected amount + $amount //= abs($expected_amount); + + # Calculate difference (actual - expected) + my $difference = $amount - abs($expected_amount); + + # Use database transaction to ensure consistency + my $schema = $self->_result->result_source->schema; + my $cashup; + + $schema->txn_do( + sub { + # Create the cashup action with actual amount + my $rs = $self->_result->add_to_cash_register_actions( + { + code => 'CASHUP', + manager_id => $manager_id, + amount => $amount + } + )->discard_changes; + + $cashup = Koha::Cash::Register::Cashup->_new_from_dbic($rs); + + # Create reconciliation accountline if there's a difference + if ( $difference != 0 ) { + + if ( $difference > 0 ) { + + # Surplus: more cash found than expected (credits are negative amounts) + my $surplus = Koha::Account::Line->new( + { + date => \'DATE_SUB(NOW(), INTERVAL 1 SECOND)', + amount => -abs($difference), # Credits are negative + description => 'Cash register surplus found during cashup', + credit_type_code => 'CASHUP_SURPLUS', + manager_id => $manager_id, + interface => 'intranet', + register_id => $self->id, + note => $reconciliation_note + } + )->store(); + + # Record the account offset + my $account_offset = Koha::Account::Offset->new( + { + credit_id => $surplus->id, + type => 'CREATE', + amount => -abs($difference) # Offsets match the line amount + } + )->store(); + + } else { + + # Deficit: less cash found than expected + my $deficit = Koha::Account::Line->new( + { + date => \'DATE_SUB(NOW(), INTERVAL 1 SECOND)', + amount => abs($difference), + description => 'Cash register deficit found during cashup', + debit_type_code => 'CASHUP_DEFICIT', + manager_id => $manager_id, + interface => 'intranet', + register_id => $self->id, + note => $reconciliation_note + } + )->store(); + my $account_offset = Koha::Account::Offset->new( + { + debit_id => $deficit->id, + type => 'CREATE', + amount => abs($difference) # Debits have positive offsets + } + )->store(); + + } + } } - )->discard_changes; + ); - return Koha::Cash::Register::Cashup->_new_from_dbic($rs); + return $cashup; } =head3 to_api_mapping diff --git a/Koha/Cash/Register/Cashup.pm b/Koha/Cash/Register/Cashup.pm index fa534286166..819bcff29f1 100644 --- a/Koha/Cash/Register/Cashup.pm +++ b/Koha/Cash/Register/Cashup.pm @@ -80,10 +80,18 @@ sub summary { : { 'date' => { '<' => $self->timestamp } }; my $payout_transactions = $self->register->accountlines->search( - { %{$conditions}, credit_type_code => undef }, + { + %{$conditions}, + credit_type_code => undef, + debit_type_code => { '!=' => 'CASHUP_DEFICIT' } + }, ); my $income_transactions = $self->register->accountlines->search( - { %{$conditions}, debit_type_code => undef }, + { + %{$conditions}, + debit_type_code => undef, + credit_type_code => { '!=' => 'CASHUP_SURPLUS' } + }, ); my $income_summary = Koha::Account::Offsets->search( @@ -173,6 +181,15 @@ sub summary { push @total_grouped, { payment_type => $type->lib, total => $typed_total }; } + # Check for reconciliation lines separately (for footer display only) + my $surplus_lines = + $self->register->accountlines->search( { %{$conditions}, credit_type_code => 'CASHUP_SURPLUS' } ); + my $deficit_lines = + $self->register->accountlines->search( { %{$conditions}, debit_type_code => 'CASHUP_DEFICIT' } ); + + my $surplus_total = $surplus_lines->count ? $surplus_lines->total : undef; + my $deficit_total = $deficit_lines->count ? $deficit_lines->total : undef; + $summary = { from_date => $previous ? $previous->timestamp : undef, to_date => $self->timestamp, @@ -181,7 +198,11 @@ sub summary { payout_grouped => \@payout, payout_total => abs($payout_total), total => $total * -1, - total_grouped => \@total_grouped + total_grouped => \@total_grouped, + + # Reconciliation data for footer display + surplus_total => $surplus_total ? $surplus_total * 1 : undef, + deficit_total => $deficit_total ? $deficit_total * 1 : undef }; return $summary; diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/cashup_summary.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/cashup_summary.inc index 960dc051566..cd503074494 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/cashup_summary.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/cashup_summary.inc @@ -11,7 +11,8 @@
  • Cash register:
  • Period: to
  • - + +
    @@ -21,6 +22,31 @@
    Type
    + +