Bugzilla – Attachment 186642 Details for
Bug 40445
Point of Sale reconciliation input during daily summaries
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 40445: Implement unified "Cashup selected" functionality with checkbox selection
Bug-40445-Implement-unified-Cashup-selected-functi.patch (text/plain), 24.23 KB, created by
Martin Renvoize (ashimema)
on 2025-09-21 11:55:08 UTC
(
hide
)
Description:
Bug 40445: Implement unified "Cashup selected" functionality with checkbox selection
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2025-09-21 11:55:08 UTC
Size:
24.23 KB
patch
obsolete
>From c1c50b93752b0efe0f707db6b8e5552833696f68 Mon Sep 17 00:00:00 2001 >From: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Date: Fri, 19 Sep 2025 13:56:28 +0100 >Subject: [PATCH] Bug 40445: Implement unified "Cashup selected" functionality > with checkbox selection > >This commit replaces the 'Cashup all' option on the branch registers page with a checkbox-based >selection system that allows staff to selectively process multiple registers using either quick >or two-phase cashup workflows. > >Changes include: > >1. Enhanced registers page template: > - Added checkbox column with "Select all" functionality > - Replaced "Cashup all" with "Cashup selected" in footer > - Added comprehensive modal offering both workflow options > - Implemented JavaScript for checkbox state management > - Added indeterminate state handling for partial selections > >2. Updated register.pl operation handlers: > - Enhanced cud-cashup_start to handle comma-separated register IDs > - Modified cud-cashup to support both single register reconciliation > and multiple register quick cashup > - Added comprehensive error handling for bulk operations > - Maintained backward compatibility with single register operations > >3. Unified approach: > - Reuses existing operations instead of creating new ones > - Supports both single and multiple register processing > - Provides appropriate redirects based on operation scope > >The implementation allows libraries to: >- Select specific registers for cashup processing >- Choose between quick cashup (immediate completion) or two-phase workflow (start now, complete later) >- Process multiple registers efficiently while maintaining individual reconciliation capability >- Handle mixed register states appropriately >--- > .../prog/en/modules/pos/registers.tt | 244 +++++++++++++++++- > pos/register.pl | 1 - > pos/registers.pl | 140 ++++++++-- > 3 files changed, 353 insertions(+), 32 deletions(-) > >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 a9d8e5c00e2..e1ce660966b 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/pos/registers.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/pos/registers.tt >@@ -45,9 +45,83 @@ > <div id="error_message" class="alert alert-warning"> You do not have permission to perform cashup actions. </div> > [% END %] > >- [% IF CAN_user_cash_management_cashup %] >- <div id="toolbar" class="btn-toolbar"> >- <button type="button" class="cashup_all btn btn-default" data-bs-toggle="modal" data-bs-target="#confirmCashupAllModal"><i class="fa-solid fa-money-bill-1"></i> Cashup all</button> >+ [% IF ( error_cashup_start && cashup_errors ) %] >+ <div class="alert alert-warning"> >+ <strong>Some registers failed to start cashup:</strong> >+ <ul> >+ [% FOREACH error IN cashup_errors %] >+ <li>[% error | html %]</li> >+ [% END %] >+ </ul> >+ </div> >+ [% END %] >+ >+ [% IF ( error_cashup_complete && cashup_errors ) %] >+ <div class="alert alert-warning"> >+ <strong>Some registers failed to complete cashup:</strong> >+ <ul> >+ [% FOREACH error IN cashup_errors %] >+ <li>[% error | html %]</li> >+ [% END %] >+ </ul> >+ </div> >+ [% END %] >+ >+ [% IF ( cashup_start_success ) %] >+ <div class="alert alert-success"> >+ <i class="fa fa-check"></i> >+ [% IF cashup_start_success == 1 %] >+ Successfully started cashup for 1 register. >+ [% ELSE %] >+ Successfully started cashup for [% cashup_start_success | html %] registers. >+ [% END %] >+ [% IF ( cashup_start_errors ) %] >+ [% IF cashup_start_errors == 1 %] >+ However, 1 register had errors. >+ [% ELSE %] >+ However, [% cashup_start_errors | html %] registers had errors. >+ [% END %] >+ [% END %] >+ </div> >+ [% END %] >+ >+ [% IF ( cashup_complete_success ) %] >+ <div class="alert alert-success"> >+ <i class="fa fa-check"></i> >+ [% IF cashup_complete_success == 1 %] >+ Successfully completed cashup for 1 register. >+ [% ELSE %] >+ Successfully completed cashup for [% cashup_complete_success | html %] registers. >+ [% END %] >+ [% IF ( cashup_complete_errors ) %] >+ [% IF cashup_complete_errors == 1 %] >+ However, 1 register had errors. >+ [% ELSE %] >+ However, [% cashup_complete_errors | html %] registers had errors. >+ [% END %] >+ [% END %] >+ </div> >+ [% END %] >+ >+ [% IF ( cashup_start_errors && !cashup_start_success ) %] >+ <div class="alert alert-warning"> >+ <i class="fa fa-exclamation-triangle"></i> >+ [% IF cashup_start_errors == 1 %] >+ Failed to start cashup for 1 register. >+ [% ELSE %] >+ Failed to start cashup for [% cashup_start_errors | html %] registers. >+ [% END %] >+ </div> >+ [% END %] >+ >+ [% IF ( cashup_complete_errors && !cashup_complete_success ) %] >+ <div class="alert alert-warning"> >+ <i class="fa fa-exclamation-triangle"></i> >+ [% IF cashup_complete_errors == 1 %] >+ Failed to complete cashup for 1 register. >+ [% ELSE %] >+ Failed to complete cashup for [% cashup_complete_errors | html %] registers. >+ [% END %] > </div> > [% END %] > >@@ -64,6 +138,9 @@ > <table id="registers" class="table_registers"> > <thead> > <tr> >+ [% IF CAN_user_cash_management_cashup %] >+ <th class="no-export"><input type="checkbox" id="select_all_registers" title="Select all available registers" /></th> >+ [% END %] > <th>Register name</th> > <th>Register description</th> > <th>Last cashup</th> >@@ -80,6 +157,15 @@ > [% SET bankable = 0, ctotal = 0, dtotal = 0, cctotal = 0, cdtotal = 0 %] > [% FOREACH register IN registers %] > <tr> >+ [% IF CAN_user_cash_management_cashup %] >+ <td> >+ [% IF register.cashup_in_progress %] >+ <input type="checkbox" class="register_checkbox" value="[% register.id | html %]" disabled title="Cashup in progress" /> >+ [% ELSE %] >+ <input type="checkbox" class="register_checkbox" value="[% register.id | html %]" /> >+ [% END %] >+ </td> >+ [% END %] > <td><a href="/cgi-bin/koha/pos/register.pl?registerid=[% register.id | uri %]">[% register.name | html %]</a></td> > <td>[% register.description | html %]</td> > <td> >@@ -144,13 +230,19 @@ > </tbody> > <tfoot> > <tr> >- <td colspan="4" align="right">Totals:</td> >+ [% IF CAN_user_cash_management_cashup %] >+ <td colspan="5" align="right">Totals:</td> >+ [% ELSE %] >+ <td colspan="4" align="right">Totals:</td> >+ [% END %] > <td>[% bankable | $Price %]</td> > <td>[% ctotal | $Price %] ([% cctotal | $Price %])</td> > <td>[% dtotal | $Price %] ([% cdtotal | $Price %])</td> > [% IF CAN_user_cash_management_cashup %] > <td> >- <button type="button" class="cashup_all btn btn-xs btn-default" data-bs-toggle="modal" data-bs-target="#confirmCashupAllModal"><i class="fa-solid fa-money-bill-1"></i> Cashup all</button> >+ <button type="button" id="cashup_selected_btn" class="btn btn-xs btn-default" data-bs-toggle="modal" data-bs-target="#confirmCashupSelectedModal" disabled >+ ><i class="fa-solid fa-money-bill-1"></i> Cashup selected</button >+ > > </td> > [% END %] > </tr> >@@ -203,6 +295,68 @@ > </div> > <!-- /#triggerCashupModalRegister --> > >+<!-- Confirm cashup selected modal --> >+<div class="modal" id="confirmCashupSelectedModal" tabindex="-1" role="dialog" aria-labelledby="confirmCashupSelectedLabel"> >+ <form method="post" class="validated"> >+ [% INCLUDE 'csrf-token.inc' %] >+ <div class="modal-dialog"> >+ <div class="modal-content"> >+ <div class="modal-header"> >+ <h1 class="modal-title" id="confirmCashupSelectedLabel">Cashup selected registers</h1> >+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> >+ </div> >+ <div class="modal-body"> >+ <p >+ ><strong>Choose how to proceed with the cashup for <span id="selected_count">0</span> selected register(s):</strong></p >+ > >+ >+ <div class="row"> >+ <div class="col-md-6"> >+ <div class="card"> >+ <div class="card-body"> >+ <h5 class="card-title"><i class="fa-solid fa-play"></i> Start cashup for selected</h5> >+ <p class="card-text">Begin two-phase cashup for all selected registers. Cash can be removed for counting while registers continue operating.</p> >+ <ul class="small"> >+ <li>Remove cash from each register for counting</li> >+ <li>Registers continue operating during counting</li> >+ <li>Complete each register individually later</li> >+ </ul> >+ </div> >+ </div> >+ </div> >+ <div class="col-md-6"> >+ <div class="card"> >+ <div class="card-body"> >+ <h5 class="card-title"><i class="fa-solid fa-lightning"></i> Quick cashup for selected</h5> >+ <p class="card-text">Complete cashup immediately for all selected registers using expected amounts (no reconciliation needed).</p> >+ <ul class="small"> >+ <li>Uses expected amounts for each register</li> >+ <li>No individual reconciliation</li> >+ <li>Completes all selected registers immediately</li> >+ </ul> >+ </div> >+ </div> >+ </div> >+ </div> >+ >+ <div class="mt-3"> >+ <h6>Selected registers:</h6> >+ <ul id="selected_registers_list"></ul> >+ </div> >+ </div> >+ <div class="modal-footer"> >+ <input type="hidden" name="registerid" id="selected_registers_field" value="" /> >+ <input type="hidden" name="op" id="selected_operation" value="" /> >+ <button type="button" class="btn btn-primary" id="start_selected_btn">Start cashup for selected</button> >+ <button type="button" class="btn btn-success" id="quick_selected_btn">Quick cashup for selected</button> >+ <button type="button" class="btn btn-default" data-bs-dismiss="modal">Cancel</button> >+ </div> >+ </div> >+ </div> >+ </form> >+</div> >+<!-- /#confirmCashupSelectedModal --> >+ > <!-- Confirm cashup modal --> > <div class="modal" id="confirmCashupModal" tabindex="-1" role="dialog" aria-labelledby="confirmCashupLabel"> > <form id="cashup_individual_form" method="post" enctype="multipart/form-data" class="validated"> >@@ -304,6 +458,7 @@ > $("#outgoing").text('[% dtotal | $Price %] ([% cdtotal | $Price %])'); > > var registers_table = $("#registers").kohaTable({ >+ columnDefs: [{ targets: [ -1, 0 ], orderable: false }], > searching: false, > paginationType: "full", > }); >@@ -400,6 +555,85 @@ > // Submit the form > form.submit(); > }); >+ >+ // Select all registers functionality >+ $("#select_all_registers").on("change", function() { >+ var isChecked = $(this).is(":checked"); >+ $(".register_checkbox:not(:disabled)").prop("checked", isChecked); >+ updateCashupSelectedButton(); >+ }); >+ >+ // Individual checkbox change handler >+ $(".register_checkbox").on("change", function() { >+ updateCashupSelectedButton(); >+ >+ // Update select all checkbox state >+ var totalCheckboxes = $(".register_checkbox:not(:disabled)").length; >+ var checkedCheckboxes = $(".register_checkbox:not(:disabled):checked").length; >+ >+ if (checkedCheckboxes === 0) { >+ $("#select_all_registers").prop("indeterminate", false).prop("checked", false); >+ } else if (checkedCheckboxes === totalCheckboxes) { >+ $("#select_all_registers").prop("indeterminate", false).prop("checked", true); >+ } else { >+ $("#select_all_registers").prop("indeterminate", true); >+ } >+ }); >+ >+ // Update cashup selected button state >+ function updateCashupSelectedButton() { >+ var selectedCount = $(".register_checkbox:checked").length; >+ var button = $("#cashup_selected_btn"); >+ >+ if (selectedCount > 0) { >+ button.prop("disabled", false).removeClass("btn-default").addClass("btn-primary"); >+ } else { >+ button.prop("disabled", true).removeClass("btn-primary").addClass("btn-default"); >+ } >+ } >+ >+ // Handle cashup selected modal >+ $("#confirmCashupSelectedModal").on("shown.bs.modal", function(e) { >+ var selectedCheckboxes = $(".register_checkbox:checked"); >+ var selectedCount = selectedCheckboxes.length; >+ var selectedIds = []; >+ var selectedNames = []; >+ >+ selectedCheckboxes.each(function() { >+ var registerRow = $(this).closest("tr"); >+ var registerId = $(this).val(); >+ var registerName = registerRow.find("td:nth-child(2) a").text(); // Second column (after checkbox) >+ >+ selectedIds.push(registerId); >+ selectedNames.push(registerName); >+ }); >+ >+ $("#selected_count").text(selectedCount); >+ $("#selected_registers_field").val(selectedIds.join(",")); >+ >+ // Populate register list >+ var listHtml = ""; >+ selectedNames.forEach(function(name) { >+ listHtml += "<li>" + name + "</li>"; >+ }); >+ $("#selected_registers_list").html(listHtml); >+ }); >+ >+ // Handle start cashup for selected >+ $("#start_selected_btn").on("click", function(e) { >+ e.preventDefault(); >+ var form = $(this).closest("form"); >+ form.find("#selected_operation").val("cud-cashup_start"); >+ form.submit(); >+ }); >+ >+ // Handle quick cashup for selected >+ $("#quick_selected_btn").on("click", function(e) { >+ e.preventDefault(); >+ var form = $(this).closest("form"); >+ form.find("#selected_operation").val("cud-cashup"); >+ form.submit(); >+ }); > }); > </script> > [% END %] >diff --git a/pos/register.pl b/pos/register.pl >index a4d8526acc5..d80f7bf2e94 100755 >--- a/pos/register.pl >+++ b/pos/register.pl >@@ -166,7 +166,6 @@ if ( !$registers->count ) { > print $input->redirect( "/cgi-bin/koha/pos/register.pl?registerid=" . $registerid ); > exit; > } >- > } else { > $template->param( error_cashup_amount => 1 ); > } >diff --git a/pos/registers.pl b/pos/registers.pl >index 49f5166bd6b..82ae9dc0026 100755 >--- a/pos/registers.pl >+++ b/pos/registers.pl >@@ -53,12 +53,39 @@ if ( !$registers->count ) { > $template->param( registers => $registers ); > } > >+# Handle success/error messages from redirects >+my $cashup_start_success = $input->param('cashup_start_success'); >+my $cashup_start_errors = $input->param('cashup_start_errors'); >+my $cashup_complete_success = $input->param('cashup_complete_success'); >+my $cashup_complete_errors = $input->param('cashup_complete_errors'); >+ >+if ($cashup_start_success) { >+ $template->param( cashup_start_success => $cashup_start_success ); >+} >+if ($cashup_start_errors) { >+ $template->param( cashup_start_errors => $cashup_start_errors ); >+} >+if ($cashup_complete_success) { >+ $template->param( cashup_complete_success => $cashup_complete_success ); >+} >+if ($cashup_complete_errors) { >+ $template->param( cashup_complete_errors => $cashup_complete_errors ); >+} >+ > my $op = $input->param('op') // ''; > if ( $op eq 'cud-cashup_start' ) { > if ( $logged_in_user->has_permission( { cash_management => 'cashup' } ) ) { >- my $registerid = $input->param('registerid'); >- if ($registerid) { >- my $register = Koha::Cash::Registers->find( { id => $registerid } ); >+ my $registerid_param = $input->param('registerid'); >+ my @register_ids = split( ',', $registerid_param ); >+ my @errors = (); >+ my $success_count = 0; >+ >+ foreach my $register_id (@register_ids) { >+ $register_id =~ s/^\s+|\s+$//g; # Trim whitespace >+ next unless $register_id; >+ >+ my $register = Koha::Cash::Registers->find( { id => $register_id } ); >+ next unless $register; > > eval { > $register->start_cashup( >@@ -66,50 +93,111 @@ if ( $op eq 'cud-cashup_start' ) { > manager_id => $logged_in_user->id, > } > ); >+ $success_count++; > }; > if ($@) { > if ( $@->isa('Koha::Exceptions::Object::DuplicateID') ) { >- $template->param( error_cashup_in_progress => 1 ); >+ push @errors, "Register " . $register->name . ": Cashup already in progress"; > } else { >- $template->param( error_cashup_start => 1 ); >+ push @errors, "Register " . $register->name . ": Failed to start cashup"; > } >- } else { >- >- # Redirect to prevent duplicate submissions (POST/REDIRECT/GET pattern) >- print $input->redirect("/cgi-bin/koha/pos/registers.pl"); >- exit; > } >+ } >+ >+ if ( @errors && $success_count == 0 ) { >+ >+ # All failed - stay on page to show errors >+ $template->param( >+ error_cashup_start => 1, >+ cashup_errors => \@errors >+ ); > } else { >- $template->param( error_missing_register_start => 1 ); >+ >+ # Some or all succeeded - redirect with coded parameters >+ my $redirect_url = "/cgi-bin/koha/pos/registers.pl"; >+ my @params; >+ >+ if ( $success_count > 0 ) { >+ push @params, "cashup_start_success=" . $success_count; >+ } >+ if (@errors) { >+ push @params, "cashup_start_errors=" . scalar(@errors); >+ } >+ >+ if (@params) { >+ $redirect_url .= "?" . join( "&", @params ); >+ } >+ >+ print $input->redirect($redirect_url); >+ exit; > } > } else { > $template->param( error_cashup_permission => 1 ); > } > } elsif ( $op eq 'cud-cashup' ) { > if ( $logged_in_user->has_permission( { cash_management => 'cashup' } ) ) { >- my $registerid = $input->param('registerid'); >- if ($registerid) { >- my $register = Koha::Cash::Registers->find( { id => $registerid } ); >- $register->add_cashup( >- { >- manager_id => $logged_in_user->id, >- amount => $register->outstanding_accountlines->total >- } >- ); >- } else { >- for my $register ( $registers->as_list ) { >+ my $registerid_param = $input->param('registerid'); >+ my @register_ids = split( ',', $registerid_param ); >+ my @errors = (); >+ my $success_count = 0; >+ >+ foreach my $register_id (@register_ids) { >+ $register_id =~ s/^\s+|\s+$//g; # Trim whitespace >+ next unless $register_id; >+ >+ my $register = Koha::Cash::Registers->find( { id => $register_id } ); >+ next unless $register; >+ >+ eval { >+ # Quick cashup: use expected amount (no amount parameter = use expected) > $register->add_cashup( > { > manager_id => $logged_in_user->id, >- amount => $register->outstanding_accountlines->total >+ >+ # No amount = uses expected amount from outstanding_accountlines >+ # No reconciliation_note = quick cashup assumes correct amounts > } > ); >+ $success_count++; >+ }; >+ if ($@) { >+ if ( $@->isa('Koha::Exceptions::Object::BadValue') ) { >+ push @errors, "Register " . $register->name . ": No cashup session to complete"; >+ } elsif ( $@->isa('Koha::Exceptions::Object::DuplicateID') ) { >+ push @errors, "Register " . $register->name . ": Cashup already completed"; >+ } else { >+ push @errors, "Register " . $register->name . ": Failed to complete cashup"; >+ } > } > } > >- # Redirect to prevent duplicate submissions (POST/REDIRECT/GET pattern) >- print $input->redirect("/cgi-bin/koha/pos/registers.pl"); >- exit; >+ if ( @errors && $success_count == 0 ) { >+ >+ # All failed - stay on page to show errors >+ $template->param( >+ error_cashup_complete => 1, >+ cashup_errors => \@errors >+ ); >+ } else { >+ >+ # Some or all succeeded - redirect with coded parameters >+ my $redirect_url = "/cgi-bin/koha/pos/registers.pl"; >+ my @params; >+ >+ if ( $success_count > 0 ) { >+ push @params, "cashup_complete_success=" . $success_count; >+ } >+ if (@errors) { >+ push @params, "cashup_complete_errors=" . scalar(@errors); >+ } >+ >+ if (@params) { >+ $redirect_url .= "?" . join( "&", @params ); >+ } >+ >+ print $input->redirect($redirect_url); >+ exit; >+ } > } else { > $template->param( error_cashup_permission => 1 ); > } >-- >2.51.0
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 40445
:
185289
|
185290
|
185291
|
185292
|
185809
|
186636
|
186637
|
186638
|
186639
|
186640
|
186641
| 186642 |
186643