From b6f0f395a2cdfaf2d58538dc3548bc2b6197b9b5 Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Mon, 16 Feb 2026 07:49:16 +0000 Subject: [PATCH] Bug 40445: Implement optional two-phase cashup workflow This patch implements an optional two-phase cashup workflow for point of sale operations, along with improved error handling and validation. Two-Phase Cashup Workflow: - Staff can initiate cashup (CASHUP_START) and complete later - Prevents new transactions on registers with cashups in progress - Tracks cashup sessions with start/end timestamps - Supports both quick cashup (immediate) and staged cashup workflows - Updates batch cashup workflow operations across multiple registers Error Handling & Validation: - Centralized exception handling for cashup operations - Clear validation messages for missing parameters - Prevents cashup when no cash transactions exist - Improved handling of zero/negative amounts - Informative error messages for user guidance Negative Amount Support: - Supports negative cashup amounts for cashup deficits - Automatic detection when actual cash is less than expected amount - Creates appropriate CASHUP_DEFICIT records - UI handles both positive and negative reconciliation amounts - Updated calculations and display logic throughout Modal Refactoring: - Eliminates code duplication between register.tt and registers.tt - Centralized cashup modal functionality in cashup_modals.js - Shared confirm_cashup.inc and trigger_cashup.inc templates - Consistent user experience across all cashup workflows - Improved maintainability and code organization Backend changes (Koha::Cash::Register): - start_cashup(): Creates CASHUP_START action to begin session - cashup_in_progress(): Detects active cashup sessions - add_cashup(): Enhanced with two-phase completion support - outstanding_accountlines(): Respects cashup session boundaries Frontend changes: - Interactive modals for starting and completing cashups - Real-time calculation displays - Support for reconciliation with actual amounts - Batch operations UI for multiple registers - Responsive error messaging and validation feedback Test plan: 1. Apply patches and restart services 2. Run prove t/db_dependent/Koha/Cash/Register.t 3. Run prove t/db_dependent/Koha/Cash/Register/Cashup.t 4. Test single register workflows: - Start cashup, add transactions, verify blocked - Complete cashup with reconciliation amounts - Test with positive, negative, and zero amounts 5. Test multi-register workflows: - Select multiple registers on registers page - Perform batch cashup operations - Verify each register processes correctly 6. Test error conditions: - Attempt cashup with no transactions - Attempt cashup with invalid amounts - Verify clear error messages displayed Sponsored-by: OpenFifth Signed-off-by: Jackie Usher --- Koha/Cash/Register.pm | 380 +++++- Koha/Cash/Register/Cashup.pm | 144 ++- .../en/includes/modals/confirm_cashup.inc | 106 ++ .../en/includes/modals/trigger_cashup.inc | 50 + .../prog/en/modules/pos/register.tt | 178 ++- .../prog/en/modules/pos/registers.tt | 307 ++++- .../prog/js/modals/cashup_modals.js | 277 +++++ pos/register.pl | 110 +- pos/registers.pl | 191 ++- t/db_dependent/Koha/Cash/Register.t | 1098 ++++++++++++++++- t/db_dependent/Koha/Cash/Register/Cashup.t | 315 ++++- 11 files changed, 2825 insertions(+), 331 deletions(-) create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/modals/confirm_cashup.inc create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/modals/trigger_cashup.inc create mode 100644 koha-tmpl/intranet-tmpl/prog/js/modals/cashup_modals.js diff --git a/Koha/Cash/Register.pm b/Koha/Cash/Register.pm index dd22ba0a7b0..611347e8568 100644 --- a/Koha/Cash/Register.pm +++ b/Koha/Cash/Register.pm @@ -16,6 +16,8 @@ package Koha::Cash::Register; # along with Koha; if not, see . use Modern::Perl; +use DateTime; +use Scalar::Util qw( looks_like_number ); use Koha::Account; use Koha::Account::Lines; @@ -23,6 +25,7 @@ use Koha::Account::Offsets; use Koha::Cash::Register::Actions; use Koha::Cash::Register::Cashups; use Koha::Database; +use Koha::DateUtils qw( dt_from_string ); use base qw(Koha::Object); @@ -113,35 +116,14 @@ Return a set of accountlines linked to this cash register since the last cashup sub outstanding_accountlines { my ( $self, $conditions, $attrs ) = @_; - my $since = $self->_result->search_related( - 'cash_register_actions', - { 'code' => 'CASHUP' }, - { - order_by => { '-desc' => [ 'timestamp', 'id' ] }, - rows => 1 - } - ); + # Find the start timestamp for the current "open" session + my $start_timestamp = $self->_get_session_start_timestamp; my $local_conditions = - $since->count - ? { 'date' => { '>' => $since->get_column('timestamp')->as_query } } + $start_timestamp + ? { 'date' => { '>' => $start_timestamp } } : {}; - # Exclude reconciliation accountlines from outstanding accountlines - $local_conditions->{'-and'} = [ - { - '-or' => [ - { 'credit_type_code' => { '!=' => 'CASHUP_SURPLUS' } }, - { 'credit_type_code' => undef } - ] - }, - { - '-or' => [ - { 'debit_type_code' => { '!=' => 'CASHUP_DEFICIT' } }, - { 'debit_type_code' => undef } - ] - } - ]; my $merged_conditions = $conditions ? { %{$conditions}, %{$local_conditions} } @@ -155,6 +137,40 @@ sub outstanding_accountlines { return Koha::Account::Lines->_new_from_dbic($rs); } +=head3 cashup_in_progress + +Check if there is currently a cashup in progress (CASHUP_START without corresponding CASHUP). +Returns the CASHUP_START action if in progress, undef otherwise. + +=cut + +sub cashup_in_progress { + my ($self) = @_; + + my $last_start = $self->_result->search_related( + 'cash_register_actions', + { 'code' => 'CASHUP_START' }, + { order_by => { '-desc' => [ 'timestamp', 'id' ] }, rows => 1 } + )->single; + + return unless $last_start; + + my $last_completion = $self->cashups( + {}, + { order_by => { '-desc' => [ 'timestamp', 'id' ] }, rows => 1 } + )->single; + + # If we have a start but no completion, or the start is more recent than completion + if ( !$last_completion + || DateTime->compare( dt_from_string( $last_start->timestamp ), dt_from_string( $last_completion->timestamp ) ) + > 0 ) + { + return Koha::Cash::Register::Action->_new_from_dbic($last_start); + } + + return; +} + =head3 store Local store method to prevent direct manipulation of the 'branch_default' field @@ -221,6 +237,87 @@ sub drop_default { return $self; } +=head3 start_cashup + + my $cashup_start = $cash_register->start_cashup( + { + manager_id => $logged_in_user->id, + } + ); + +Start a new cashup period. This marks the beginning of the cash counting process +and creates a snapshot point for calculating outstanding amounts. Returns the +CASHUP_START action. + +=cut + +sub start_cashup { + my ( $self, $params ) = @_; + + # check for mandatory params + my @mandatory = ('manager_id'); + for my $param (@mandatory) { + unless ( defined( $params->{$param} ) ) { + Koha::Exceptions::MissingParameter->throw( error => "The $param parameter is mandatory" ); + } + } + my $manager_id = $params->{manager_id}; + + # Check if there's already a cashup in progress + my $last_cashup_start_rs = $self->_result->search_related( + 'cash_register_actions', + { 'code' => 'CASHUP_START' }, + { order_by => { '-desc' => [ 'timestamp', 'id' ] }, rows => 1 } + )->single; + + my $last_cashup_completed = $self->cashups( + {}, + { order_by => { '-desc' => [ 'timestamp', 'id' ] }, rows => 1 } + )->single; + + # If we have a CASHUP_START that's more recent than the last CASHUP, there's already an active cashup + if ( + $last_cashup_start_rs + && ( + !$last_cashup_completed || DateTime->compare( + dt_from_string( $last_cashup_start_rs->timestamp ), + dt_from_string( $last_cashup_completed->timestamp ) + ) > 0 + ) + ) + { + Koha::Exceptions::Object::DuplicateID->throw( error => "A cashup is already in progress for this register" ); + } + + my $expected_amount = $self->outstanding_accountlines->total( { payment_type => [ 'CASH', 'SIP00' ] } ) * -1; + + # Prevent starting a cashup when there are no transactions at all + my $total_transactions = $self->outstanding_accountlines->total() * -1; + unless ( $total_transactions != 0 ) { + Koha::Exceptions::Object::BadValue->throw( + error => "Cannot start cashup with no transactions", + type => 'amount', + value => $total_transactions + ); + } + + # Create the CASHUP_START action using centralized exception handling + my $schema = $self->_result->result_source->schema; + my $rs = $schema->safe_do( + sub { + return $self->_result->add_to_cash_register_actions( + { + code => 'CASHUP_START', + manager_id => $manager_id, + amount => $expected_amount + } + )->discard_changes; + } + ); + + return Koha::Cash::Register::Cashup->_new_from_dbic($rs); +} + =head3 add_cashup my $cashup = $cash_register->add_cashup( @@ -231,33 +328,83 @@ sub drop_default { } ); -Add a new cashup action to the till, returns the added action. -If amount differs from expected amount, creates surplus/deficit accountlines. +Complete a cashup period started with start_cashup(). This performs the actual +reconciliation against the amount counted and creates surplus/deficit accountlines +if needed. Returns the completed CASHUP action. =cut sub add_cashup { my ( $self, $params ) = @_; - my $manager_id = $params->{manager_id}; - my $amount = $params->{amount}; - my $reconciliation_note = $params->{reconciliation_note}; + # check for mandatory params + my @mandatory = ( 'manager_id', 'amount' ); + for my $param (@mandatory) { + unless ( defined( $params->{$param} ) ) { + Koha::Exceptions::MissingParameter->throw( error => "The $param parameter is mandatory" ); + } + } + my $manager_id = $params->{manager_id}; + + # Validate amount is a valid number + my $amount = $params->{amount}; + unless ( looks_like_number($amount) ) { + Koha::Exceptions::Account::AmountNotPositive->throw( error => 'Cashup amount must be a valid number' ); + } # Sanitize reconciliation note - treat empty/whitespace-only as undef + my $reconciliation_note = $params->{reconciliation_note}; 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; + # Find the most recent CASHUP_START to determine if we're in two-phase mode + my $cashup_start; + my $cashup_start_rs = $self->_result->search_related( + 'cash_register_actions', + { 'code' => 'CASHUP_START' }, + { order_by => { '-desc' => [ 'timestamp', 'id' ] }, rows => 1 } + )->single; + + if ($cashup_start_rs) { + + # Two-phase mode: Check if this CASHUP_START has already been completed + my $existing_completion = $self->_result->search_related( + 'cash_register_actions', + { + 'code' => 'CASHUP', + 'timestamp' => { '>' => $cashup_start_rs->timestamp } + }, + { rows => 1 } + )->single; + + if ( !$existing_completion ) { + $cashup_start = Koha::Cash::Register::Cashup->_new_from_dbic($cashup_start_rs); + } + + } - # For backward compatibility, if no actual amount is specified, use expected amount - $amount //= abs($expected_amount); + # Calculate expected amount from session accountlines + my $expected_amount = ( + $cashup_start + ? $cashup_start->accountlines->total( { payment_type => [ 'CASH', 'SIP00' ] } ) + : $self->outstanding_accountlines->total( { payment_type => [ 'CASH', 'SIP00' ] } ) + ) * -1; # Calculate difference (actual - expected) - my $difference = $amount - abs($expected_amount); + my $difference = $amount - $expected_amount; + + # Validate reconciliation note requirement if there's a discrepancy + if ( $difference != 0 ) { + my $note_required = C4::Context->preference('CashupReconciliationNoteRequired') // 0; + + if ( $note_required && !defined $reconciliation_note ) { + Koha::Exceptions::MissingParameter->throw( + error => "Reconciliation note is required when cashup amount differs from expected amount" ); + } + } # Use database transaction to ensure consistency my $schema = $self->_result->result_source->schema; @@ -265,36 +412,52 @@ sub add_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 + # Create the cashup action - safe_do handles exception translation + my $rs = $schema->safe_do( + sub { + return $self->_result->add_to_cash_register_actions( + { + code => 'CASHUP', + manager_id => $manager_id, + amount => $amount + } + )->discard_changes; } - )->discard_changes; - + ); $cashup = Koha::Cash::Register::Cashup->_new_from_dbic($rs); # Create reconciliation accountline if there's a difference if ( $difference != 0 ) { + # Determine reconciliation date based on mode + my $reconciliation_date; + if ($cashup_start) { + + # Two-phase mode: Backdate reconciliation lines to just before the CASHUP_START timestamp + # This ensures they belong to the previous session, not the current one + my $timestamp_str = "DATE_SUB('" . $cashup_start->timestamp . "', INTERVAL 1 SECOND)"; + $reconciliation_date = \$timestamp_str; + } else { + + # Legacy mode: Use the original backdating approach + $reconciliation_date = \'DATE_SUB(NOW(), INTERVAL 1 SECOND)'; + } + 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 - amountoutstanding => 0, - description => 'Cash register surplus found during cashup', - credit_type_code => 'CASHUP_SURPLUS', - payment_type => 'CASH', - manager_id => $manager_id, - interface => 'intranet', - branchcode => $self->branch, - register_id => $self->id, - note => $reconciliation_note + date => $reconciliation_date, + amount => -abs($difference), # Credits are negative + amountoutstanding => 0, + credit_type_code => 'CASHUP_SURPLUS', + manager_id => $manager_id, + interface => 'intranet', + branchcode => $self->branch, + register_id => $self->id, + payment_type => 'CASH', + note => $reconciliation_note } )->store(); @@ -312,17 +475,16 @@ sub add_cashup { # Deficit: less cash found than expected my $deficit = Koha::Account::Line->new( { - date => \'DATE_SUB(NOW(), INTERVAL 1 SECOND)', - amount => abs($difference), - amountoutstanding => 0, - description => 'Cash register deficit found during cashup', - debit_type_code => 'CASHUP_DEFICIT', - payment_type => 'CASH', - manager_id => $manager_id, - interface => 'intranet', - branchcode => $self->branch, - register_id => $self->id, - note => $reconciliation_note + date => $reconciliation_date, + amount => abs($difference), + amountoutstanding => 0, + debit_type_code => 'CASHUP_DEFICIT', + manager_id => $manager_id, + interface => 'intranet', + branchcode => $self->branch, + register_id => $self->id, + payment_type => 'CASH', + note => $reconciliation_note } )->store(); my $account_offset = Koha::Account::Offset->new( @@ -341,6 +503,94 @@ sub add_cashup { return $cashup; } +=head3 _get_session_start_timestamp + +Internal method to determine the start timestamp for the current "open" session. +This handles the following cashup scenarios: + +=over 4 + +=item 1. No cashups ever → undef (returns all accountlines) + +=item 2. Quick cashup completed → Uses CASHUP timestamp + +=item 3. Two-phase started → Uses CASHUP_START timestamp + +=item 4. Two-phase completed → Uses the CASHUP_START timestamp that led to the last CASHUP + +=item 5. Mixed workflows → Correctly distinguishes between quick and two-phase cashups + +=back + +=cut + +sub _get_session_start_timestamp { + my ($self) = @_; + + # Check if there's a cashup in progress (CASHUP_START without corresponding CASHUP) + my $cashup_in_progress = $self->cashup_in_progress; + + if ($cashup_in_progress) { + + # Scenario 3: Two-phase cashup started - return accountlines since CASHUP_START + return $cashup_in_progress->timestamp; + } + + # No cashup in progress - find the most recent cashup completion + my $last_cashup = $self->cashups( + {}, + { + order_by => { '-desc' => [ 'timestamp', 'id' ] }, + rows => 1 + } + )->single; + + if ( !$last_cashup ) { + + # Scenario 1: No cashups have ever taken place - return all accountlines + return; + } + + # Find if this CASHUP was part of a two-phase workflow + my $corresponding_start = $self->_result->search_related( + 'cash_register_actions', + { + 'code' => 'CASHUP_START', + 'timestamp' => { '<' => $last_cashup->timestamp } + }, + { + order_by => { '-desc' => [ 'timestamp', 'id' ] }, + rows => 1 + } + )->single; + + if ($corresponding_start) { + + # Check if this CASHUP_START was completed by this CASHUP + # (no other CASHUP between them) + my $intervening_cashup = $self->_result->search_related( + 'cash_register_actions', + { + 'code' => 'CASHUP', + 'timestamp' => { + '>' => $corresponding_start->timestamp, + '<' => $last_cashup->timestamp + } + }, + { rows => 1 } + )->single; + + if ( !$intervening_cashup ) { + + # Scenario 4: Two-phase cashup completed - return accountlines since the CASHUP_START + return $corresponding_start->timestamp; + } + } + + # Scenarios 2 & 5: Quick cashup (or orphaned CASHUP) - return accountlines since CASHUP + return $last_cashup->timestamp; +} + =head3 to_api_mapping This method returns the mapping for representing a Koha::Cash::Register object diff --git a/Koha/Cash/Register/Cashup.pm b/Koha/Cash/Register/Cashup.pm index 35367230a0e..01ffb9fcb48 100644 --- a/Koha/Cash/Register/Cashup.pm +++ b/Koha/Cash/Register/Cashup.pm @@ -61,23 +61,29 @@ Return a hashref containing a summary of transactions that make up this cashup. sub summary { my ($self) = @_; my $summary; - my $prior_cashup = Koha::Cash::Register::Cashups->search( - { - 'timestamp' => { '<' => $self->timestamp }, - register_id => $self->register_id - }, - { - order_by => { '-desc' => [ 'timestamp', 'id' ] }, - rows => 1 - } - ); - my $previous = $prior_cashup->single; + # Get the session boundaries for this cashup + my ( $session_start, $session_end ) = $self->_get_session_boundaries; - my $conditions = - $previous - ? { 'date' => { '-between' => [ $previous->_result->get_column('timestamp'), $self->timestamp ] } } - : { 'date' => { '<' => $self->timestamp } }; + my $conditions; + if ( $session_start && $session_end ) { + + # Complete session: between start and end (exclusive) + $conditions = { + 'date' => { + '>' => $session_start, + '<' => $session_end + } + }; + } elsif ($session_end) { + + # Session from beginning to end + $conditions = { 'date' => { '<' => $session_end } }; + } else { + + # Shouldn't happen for a completed cashup, but fallback + $conditions = { 'date' => { '<' => $self->timestamp } }; + } my $payout_transactions = $self->register->accountlines->search( { @@ -198,8 +204,8 @@ sub summary { my $deficit_note = $deficit_record ? $deficit_record->note : undef; $summary = { - from_date => $previous ? $previous->timestamp : undef, - to_date => $self->timestamp, + from_date => $session_start, + to_date => $session_end, income_grouped => \@income, income_total => abs($income_total), payout_grouped => \@payout, @@ -217,6 +223,110 @@ sub summary { return $summary; } +=head3 accountlines + +Fetch the accountlines associated with this cashup + +=cut + +sub accountlines { + my ($self) = @_; + + # Get the session boundaries for this cashup + my ( $session_start, $session_end ) = $self->_get_session_boundaries; + + my $conditions; + if ( $session_start && $session_end ) { + + # Complete session: between start and end (exclusive) + $conditions = { + 'date' => { + '>' => $session_start, + '<' => $session_end + } + }; + } elsif ($session_end) { + + # Session from beginning to end + $conditions = { 'date' => { '<' => $session_end } }; + } else { + + # Shouldn't happen for a completed cashup, but fallback + $conditions = { 'date' => { '<' => $self->timestamp } }; + } + + return $self->register->accountlines->search($conditions); +} + +=head3 _get_session_boundaries + +Internal method to determine the session boundaries for this cashup. +Returns ($session_start, $session_end) timestamps. + +=cut + +sub _get_session_boundaries { + my ($self) = @_; + + my $session_end = $self->_get_session_end; + + # Find the previous CASHUP + my $session_start; + my $previous_cashup = $self->register->cashups( + { 'timestamp' => { '<' => $session_end } }, + { + order_by => { '-desc' => [ 'timestamp', 'id' ] }, + rows => 1 + } + )->single; + + $session_start = $previous_cashup ? $previous_cashup->_get_session_end : undef; + + return ( $session_start, $session_end ); +} + +sub _get_session_end { + my ($self) = @_; + + my $session_end = $self->timestamp; + + # Find if this CASHUP was part of a two-phase workflow + my $nearest_start = $self->register->_result->search_related( + 'cash_register_actions', + { + 'code' => 'CASHUP_START', + 'timestamp' => { '<' => $session_end } + }, + { + order_by => { '-desc' => [ 'timestamp', 'id' ] }, + rows => 1 + } + )->single; + + if ($nearest_start) { + + # Check if this CASHUP_START was completed by this CASHUP + # (no other CASHUP between them) + my $intervening_cashup = $self->register->cashups( + { + 'timestamp' => { + '>' => $nearest_start->timestamp, + '<' => $session_end + } + }, + { rows => 1 } + )->single; + + if ( !$intervening_cashup ) { + + # Two-phase workflow: session runs to CASHUP_START + $session_end = $nearest_start->timestamp; + } + } + + return $session_end; +} + =head3 to_api_mapping This method returns the mapping for representing a Koha::Cash::Register::Cashup object diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/confirm_cashup.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/confirm_cashup.inc new file mode 100644 index 00000000000..e0f4e53c377 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/confirm_cashup.inc @@ -0,0 +1,106 @@ +[% USE raw %] + + + + diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/trigger_cashup.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/trigger_cashup.inc new file mode 100644 index 00000000000..302e985f5c6 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/trigger_cashup.inc @@ -0,0 +1,50 @@ +[% USE raw %] + + + + 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 4464c1a9db7..13fe42f07cb 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/pos/register.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/pos/register.tt @@ -52,13 +52,73 @@
Invalid amount entered for cashup. Please enter a valid monetary amount.
[% END %] + [% IF ( error_cashup_in_progress ) %] +
A cashup is already in progress for this register.
+ [% END %] + + [% IF ( error_cashup_no_transactions ) %] +
Cannot start cashup - there are no transactions in this register since the last cashup.
+ [% END %] + + [% IF ( error_no_cashup_start ) %] +
No cashup session has been started. Please start a cashup before attempting to complete it.
+ [% END %] + + [% IF ( error_cashup_already_completed ) %] +
This cashup session has already been completed.
+ [% END %] + + [% IF ( error_cashup_start ) %] +
Failed to start cashup. Please try again.
+ [% END %] + + [% IF ( error_cashup_missing_param ) %] +
Missing required parameter for cashup: [% error_message | html %]
+ [% END %] + + [% IF ( error_cashup_amount_invalid ) %] +
The cashup amount must be a valid number.
+ [% END %] + + [% IF ( error_reconciliation_note_required ) %] +
Reconciliation note is required when cashup amount differs from expected amount.
+ [% END %] + + [% IF ( error_cashup_complete ) %] +
+ Failed to complete cashup. Please try again. + [% IF error_details %] +
Error details: [% error_details | html %] + [% END %] +
+ [% END %] + [% IF ( error_refund_permission ) %]
You do not have permission to perform refund actions.
[% END %] + [% IF cashup_in_progress %] +
+ + [% SET progress_timestamp = cashup_in_progress.timestamp | $KohaDates(with_hours => 1) %] + [% tx("Cashup in progress - started {timestamp}. You can continue to make transactions while counting cash.", { timestamp = progress_timestamp }) | html %] + ([% t("Preview cashup summary") | html %]) +
+ [% END %] + + [% SET total_bankable = accountlines.total( payment_type => [ 'CASH', 'SIP00' ]) * -1 %] + [% SET total_transactions = accountlines.total() * -1 %] [% IF ( CAN_user_cash_management_cashup ) %]
- + [% IF cashup_in_progress %] + + [% ELSE %] + + [% END %]
[% END %] @@ -81,7 +141,7 @@
  • Float: [% register.starting_float | $Price %]
  • Total income (cash): [% accountlines.credits_total * -1 | $Price %] ([% accountlines.credits_total(payment_type => [ 'CASH', 'SIP00' ]) * -1 | $Price %])
  • Total outgoing (cash): [% accountlines.debits_total * -1 | $Price %] ([% accountlines.debits_total( payment_type => [ 'CASH', 'SIP00' ]) * -1 | $Price %])
  • -
  • Total bankable: [% accountlines.total( payment_type => [ 'CASH', 'SIP00' ]) * -1 | $Price %]
  • +
  • Total bankable: [% total_bankable | $Price %]
  • [% IF register.last_cashup %] @@ -368,60 +428,6 @@ [% END %] [% END %] - - - -