View | Details | Raw Unified | Return to bug 40445
Collapse All | Expand All

(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/pos/registers.tt (-5 / +239 lines)
Lines 45-53 Link Here
45
            <div id="error_message" class="alert alert-warning"> You do not have permission to perform cashup actions. </div>
45
            <div id="error_message" class="alert alert-warning"> You do not have permission to perform cashup actions. </div>
46
        [% END %]
46
        [% END %]
47
47
48
        [% IF CAN_user_cash_management_cashup %]
48
        [% IF ( error_cashup_start && cashup_errors ) %]
49
            <div id="toolbar" class="btn-toolbar">
49
            <div class="alert alert-warning">
50
                <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>
50
                <strong>Some registers failed to start cashup:</strong>
51
                <ul>
52
                    [% FOREACH error IN cashup_errors %]
53
                        <li>[% error | html %]</li>
54
                    [% END %]
55
                </ul>
56
            </div>
57
        [% END %]
58
59
        [% IF ( error_cashup_complete && cashup_errors ) %]
60
            <div class="alert alert-warning">
61
                <strong>Some registers failed to complete cashup:</strong>
62
                <ul>
63
                    [% FOREACH error IN cashup_errors %]
64
                        <li>[% error | html %]</li>
65
                    [% END %]
66
                </ul>
67
            </div>
68
        [% END %]
69
70
        [% IF ( cashup_start_success ) %]
71
            <div class="alert alert-success">
72
                <i class="fa fa-check"></i>
73
                [% IF cashup_start_success == 1 %]
74
                    Successfully started cashup for 1 register.
75
                [% ELSE %]
76
                    Successfully started cashup for [% cashup_start_success | html %] registers.
77
                [% END %]
78
                [% IF ( cashup_start_errors ) %]
79
                    [% IF cashup_start_errors == 1 %]
80
                        However, 1 register had errors.
81
                    [% ELSE %]
82
                        However, [% cashup_start_errors | html %] registers had errors.
83
                    [% END %]
84
                [% END %]
85
            </div>
86
        [% END %]
87
88
        [% IF ( cashup_complete_success ) %]
89
            <div class="alert alert-success">
90
                <i class="fa fa-check"></i>
91
                [% IF cashup_complete_success == 1 %]
92
                    Successfully completed cashup for 1 register.
93
                [% ELSE %]
94
                    Successfully completed cashup for [% cashup_complete_success | html %] registers.
95
                [% END %]
96
                [% IF ( cashup_complete_errors ) %]
97
                    [% IF cashup_complete_errors == 1 %]
98
                        However, 1 register had errors.
99
                    [% ELSE %]
100
                        However, [% cashup_complete_errors | html %] registers had errors.
101
                    [% END %]
102
                [% END %]
103
            </div>
104
        [% END %]
105
106
        [% IF ( cashup_start_errors && !cashup_start_success ) %]
107
            <div class="alert alert-warning">
108
                <i class="fa fa-exclamation-triangle"></i>
109
                [% IF cashup_start_errors == 1 %]
110
                    Failed to start cashup for 1 register.
111
                [% ELSE %]
112
                    Failed to start cashup for [% cashup_start_errors | html %] registers.
113
                [% END %]
114
            </div>
115
        [% END %]
116
117
        [% IF ( cashup_complete_errors && !cashup_complete_success ) %]
118
            <div class="alert alert-warning">
119
                <i class="fa fa-exclamation-triangle"></i>
120
                [% IF cashup_complete_errors == 1 %]
121
                    Failed to complete cashup for 1 register.
122
                [% ELSE %]
123
                    Failed to complete cashup for [% cashup_complete_errors | html %] registers.
124
                [% END %]
51
            </div>
125
            </div>
52
        [% END %]
126
        [% END %]
53
127
Lines 64-69 Link Here
64
            <table id="registers" class="table_registers">
138
            <table id="registers" class="table_registers">
65
                <thead>
139
                <thead>
66
                    <tr>
140
                    <tr>
141
                        [% IF CAN_user_cash_management_cashup %]
142
                            <th class="no-export"><input type="checkbox" id="select_all_registers" title="Select all available registers" /></th>
143
                        [% END %]
67
                        <th>Register name</th>
144
                        <th>Register name</th>
68
                        <th>Register description</th>
145
                        <th>Register description</th>
69
                        <th>Last cashup</th>
146
                        <th>Last cashup</th>
Lines 80-85 Link Here
80
                    [% SET bankable = 0, ctotal = 0, dtotal = 0, cctotal = 0, cdtotal = 0 %]
157
                    [% SET bankable = 0, ctotal = 0, dtotal = 0, cctotal = 0, cdtotal = 0 %]
81
                    [% FOREACH register IN registers %]
158
                    [% FOREACH register IN registers %]
82
                        <tr>
159
                        <tr>
160
                            [% IF CAN_user_cash_management_cashup %]
161
                                <td>
162
                                    [% IF register.cashup_in_progress %]
163
                                        <input type="checkbox" class="register_checkbox" value="[% register.id | html %]" disabled title="Cashup in progress" />
164
                                    [% ELSE %]
165
                                        <input type="checkbox" class="register_checkbox" value="[% register.id | html %]" />
166
                                    [% END %]
167
                                </td>
168
                            [% END %]
83
                            <td><a href="/cgi-bin/koha/pos/register.pl?registerid=[% register.id | uri %]">[% register.name | html %]</a></td>
169
                            <td><a href="/cgi-bin/koha/pos/register.pl?registerid=[% register.id | uri %]">[% register.name | html %]</a></td>
84
                            <td>[% register.description | html %]</td>
170
                            <td>[% register.description | html %]</td>
85
                            <td>
171
                            <td>
Lines 144-156 Link Here
144
                </tbody>
230
                </tbody>
145
                <tfoot>
231
                <tfoot>
146
                    <tr>
232
                    <tr>
147
                        <td colspan="4" align="right">Totals:</td>
233
                        [% IF CAN_user_cash_management_cashup %]
234
                            <td colspan="5" align="right">Totals:</td>
235
                        [% ELSE %]
236
                            <td colspan="4" align="right">Totals:</td>
237
                        [% END %]
148
                        <td>[% bankable | $Price %]</td>
238
                        <td>[% bankable | $Price %]</td>
149
                        <td>[% ctotal | $Price %] ([% cctotal | $Price %])</td>
239
                        <td>[% ctotal | $Price %] ([% cctotal | $Price %])</td>
150
                        <td>[% dtotal | $Price %] ([% cdtotal | $Price %])</td>
240
                        <td>[% dtotal | $Price %] ([% cdtotal | $Price %])</td>
151
                        [% IF CAN_user_cash_management_cashup %]
241
                        [% IF CAN_user_cash_management_cashup %]
152
                            <td>
242
                            <td>
153
                                <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>
243
                                <button type="button" id="cashup_selected_btn" class="btn btn-xs btn-default" data-bs-toggle="modal" data-bs-target="#confirmCashupSelectedModal" disabled
244
                                    ><i class="fa-solid fa-money-bill-1"></i> Cashup selected</button
245
                                >
154
                            </td>
246
                            </td>
155
                        [% END %]
247
                        [% END %]
156
                    </tr>
248
                    </tr>
Lines 203-208 Link Here
203
</div>
295
</div>
204
<!-- /#triggerCashupModalRegister -->
296
<!-- /#triggerCashupModalRegister -->
205
297
298
<!-- Confirm cashup selected modal -->
299
<div class="modal" id="confirmCashupSelectedModal" tabindex="-1" role="dialog" aria-labelledby="confirmCashupSelectedLabel">
300
    <form method="post" class="validated">
301
        [% INCLUDE 'csrf-token.inc' %]
302
        <div class="modal-dialog">
303
            <div class="modal-content">
304
                <div class="modal-header">
305
                    <h1 class="modal-title" id="confirmCashupSelectedLabel">Cashup selected registers</h1>
306
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
307
                </div>
308
                <div class="modal-body">
309
                    <p
310
                        ><strong>Choose how to proceed with the cashup for <span id="selected_count">0</span> selected register(s):</strong></p
311
                    >
312
313
                    <div class="row">
314
                        <div class="col-md-6">
315
                            <div class="card">
316
                                <div class="card-body">
317
                                    <h5 class="card-title"><i class="fa-solid fa-play"></i> Start cashup for selected</h5>
318
                                    <p class="card-text">Begin two-phase cashup for all selected registers. Cash can be removed for counting while registers continue operating.</p>
319
                                    <ul class="small">
320
                                        <li>Remove cash from each register for counting</li>
321
                                        <li>Registers continue operating during counting</li>
322
                                        <li>Complete each register individually later</li>
323
                                    </ul>
324
                                </div>
325
                            </div>
326
                        </div>
327
                        <div class="col-md-6">
328
                            <div class="card">
329
                                <div class="card-body">
330
                                    <h5 class="card-title"><i class="fa-solid fa-lightning"></i> Quick cashup for selected</h5>
331
                                    <p class="card-text">Complete cashup immediately for all selected registers using expected amounts (no reconciliation needed).</p>
332
                                    <ul class="small">
333
                                        <li>Uses expected amounts for each register</li>
334
                                        <li>No individual reconciliation</li>
335
                                        <li>Completes all selected registers immediately</li>
336
                                    </ul>
337
                                </div>
338
                            </div>
339
                        </div>
340
                    </div>
341
342
                    <div class="mt-3">
343
                        <h6>Selected registers:</h6>
344
                        <ul id="selected_registers_list"></ul>
345
                    </div>
346
                </div>
347
                <div class="modal-footer">
348
                    <input type="hidden" name="registerid" id="selected_registers_field" value="" />
349
                    <input type="hidden" name="op" id="selected_operation" value="" />
350
                    <button type="button" class="btn btn-primary" id="start_selected_btn">Start cashup for selected</button>
351
                    <button type="button" class="btn btn-success" id="quick_selected_btn">Quick cashup for selected</button>
352
                    <button type="button" class="btn btn-default" data-bs-dismiss="modal">Cancel</button>
353
                </div>
354
            </div>
355
        </div>
356
    </form>
357
</div>
358
<!-- /#confirmCashupSelectedModal -->
359
206
<!-- Confirm cashup modal -->
360
<!-- Confirm cashup modal -->
207
<div class="modal" id="confirmCashupModal" tabindex="-1" role="dialog" aria-labelledby="confirmCashupLabel">
361
<div class="modal" id="confirmCashupModal" tabindex="-1" role="dialog" aria-labelledby="confirmCashupLabel">
208
    <form id="cashup_individual_form" method="post" enctype="multipart/form-data" class="validated">
362
    <form id="cashup_individual_form" method="post" enctype="multipart/form-data" class="validated">
Lines 304-309 Link Here
304
            $("#outgoing").text('[% dtotal | $Price %] ([% cdtotal | $Price %])');
458
            $("#outgoing").text('[% dtotal | $Price %] ([% cdtotal | $Price %])');
305
459
306
            var registers_table = $("#registers").kohaTable({
460
            var registers_table = $("#registers").kohaTable({
461
                columnDefs: [{ targets: [ -1, 0 ], orderable: false }],
307
                searching: false,
462
                searching: false,
308
                paginationType: "full",
463
                paginationType: "full",
309
            });
464
            });
Lines 400-405 Link Here
400
               // Submit the form
555
               // Submit the form
401
               form.submit();
556
               form.submit();
402
            });
557
            });
558
559
            // Select all registers functionality
560
            $("#select_all_registers").on("change", function() {
561
                var isChecked = $(this).is(":checked");
562
                $(".register_checkbox:not(:disabled)").prop("checked", isChecked);
563
                updateCashupSelectedButton();
564
            });
565
566
            // Individual checkbox change handler
567
            $(".register_checkbox").on("change", function() {
568
                updateCashupSelectedButton();
569
570
                // Update select all checkbox state
571
                var totalCheckboxes = $(".register_checkbox:not(:disabled)").length;
572
                var checkedCheckboxes = $(".register_checkbox:not(:disabled):checked").length;
573
574
                if (checkedCheckboxes === 0) {
575
                    $("#select_all_registers").prop("indeterminate", false).prop("checked", false);
576
                } else if (checkedCheckboxes === totalCheckboxes) {
577
                    $("#select_all_registers").prop("indeterminate", false).prop("checked", true);
578
                } else {
579
                    $("#select_all_registers").prop("indeterminate", true);
580
                }
581
            });
582
583
            // Update cashup selected button state
584
            function updateCashupSelectedButton() {
585
                var selectedCount = $(".register_checkbox:checked").length;
586
                var button = $("#cashup_selected_btn");
587
588
                if (selectedCount > 0) {
589
                    button.prop("disabled", false).removeClass("btn-default").addClass("btn-primary");
590
                } else {
591
                    button.prop("disabled", true).removeClass("btn-primary").addClass("btn-default");
592
                }
593
            }
594
595
            // Handle cashup selected modal
596
            $("#confirmCashupSelectedModal").on("shown.bs.modal", function(e) {
597
                var selectedCheckboxes = $(".register_checkbox:checked");
598
                var selectedCount = selectedCheckboxes.length;
599
                var selectedIds = [];
600
                var selectedNames = [];
601
602
                selectedCheckboxes.each(function() {
603
                    var registerRow = $(this).closest("tr");
604
                    var registerId = $(this).val();
605
                    var registerName = registerRow.find("td:nth-child(2) a").text(); // Second column (after checkbox)
606
607
                    selectedIds.push(registerId);
608
                    selectedNames.push(registerName);
609
                });
610
611
                $("#selected_count").text(selectedCount);
612
                $("#selected_registers_field").val(selectedIds.join(","));
613
614
                // Populate register list
615
                var listHtml = "";
616
                selectedNames.forEach(function(name) {
617
                    listHtml += "<li>" + name + "</li>";
618
                });
619
                $("#selected_registers_list").html(listHtml);
620
            });
621
622
            // Handle start cashup for selected
623
            $("#start_selected_btn").on("click", function(e) {
624
                e.preventDefault();
625
                var form = $(this).closest("form");
626
                form.find("#selected_operation").val("cud-cashup_start");
627
                form.submit();
628
            });
629
630
            // Handle quick cashup for selected
631
            $("#quick_selected_btn").on("click", function(e) {
632
                e.preventDefault();
633
                var form = $(this).closest("form");
634
                form.find("#selected_operation").val("cud-cashup");
635
                form.submit();
636
            });
403
        });
637
        });
404
    </script>
638
    </script>
405
[% END %]
639
[% END %]
(-)a/pos/register.pl (-1 lines)
Lines 166-172 if ( !$registers->count ) { Link Here
166
                    print $input->redirect( "/cgi-bin/koha/pos/register.pl?registerid=" . $registerid );
166
                    print $input->redirect( "/cgi-bin/koha/pos/register.pl?registerid=" . $registerid );
167
                    exit;
167
                    exit;
168
                }
168
                }
169
170
            } else {
169
            } else {
171
                $template->param( error_cashup_amount => 1 );
170
                $template->param( error_cashup_amount => 1 );
172
            }
171
            }
(-)a/pos/registers.pl (-27 / +114 lines)
Lines 53-64 if ( !$registers->count ) { Link Here
53
    $template->param( registers => $registers );
53
    $template->param( registers => $registers );
54
}
54
}
55
55
56
# Handle success/error messages from redirects
57
my $cashup_start_success    = $input->param('cashup_start_success');
58
my $cashup_start_errors     = $input->param('cashup_start_errors');
59
my $cashup_complete_success = $input->param('cashup_complete_success');
60
my $cashup_complete_errors  = $input->param('cashup_complete_errors');
61
62
if ($cashup_start_success) {
63
    $template->param( cashup_start_success => $cashup_start_success );
64
}
65
if ($cashup_start_errors) {
66
    $template->param( cashup_start_errors => $cashup_start_errors );
67
}
68
if ($cashup_complete_success) {
69
    $template->param( cashup_complete_success => $cashup_complete_success );
70
}
71
if ($cashup_complete_errors) {
72
    $template->param( cashup_complete_errors => $cashup_complete_errors );
73
}
74
56
my $op = $input->param('op') // '';
75
my $op = $input->param('op') // '';
57
if ( $op eq 'cud-cashup_start' ) {
76
if ( $op eq 'cud-cashup_start' ) {
58
    if ( $logged_in_user->has_permission( { cash_management => 'cashup' } ) ) {
77
    if ( $logged_in_user->has_permission( { cash_management => 'cashup' } ) ) {
59
        my $registerid = $input->param('registerid');
78
        my $registerid_param = $input->param('registerid');
60
        if ($registerid) {
79
        my @register_ids     = split( ',', $registerid_param );
61
            my $register = Koha::Cash::Registers->find( { id => $registerid } );
80
        my @errors           = ();
81
        my $success_count    = 0;
82
83
        foreach my $register_id (@register_ids) {
84
            $register_id =~ s/^\s+|\s+$//g;    # Trim whitespace
85
            next unless $register_id;
86
87
            my $register = Koha::Cash::Registers->find( { id => $register_id } );
88
            next unless $register;
62
89
63
            eval {
90
            eval {
64
                $register->start_cashup(
91
                $register->start_cashup(
Lines 66-115 if ( $op eq 'cud-cashup_start' ) { Link Here
66
                        manager_id => $logged_in_user->id,
93
                        manager_id => $logged_in_user->id,
67
                    }
94
                    }
68
                );
95
                );
96
                $success_count++;
69
            };
97
            };
70
            if ($@) {
98
            if ($@) {
71
                if ( $@->isa('Koha::Exceptions::Object::DuplicateID') ) {
99
                if ( $@->isa('Koha::Exceptions::Object::DuplicateID') ) {
72
                    $template->param( error_cashup_in_progress => 1 );
100
                    push @errors, "Register " . $register->name . ": Cashup already in progress";
73
                } else {
101
                } else {
74
                    $template->param( error_cashup_start => 1 );
102
                    push @errors, "Register " . $register->name . ": Failed to start cashup";
75
                }
103
                }
76
            } else {
77
78
                # Redirect to prevent duplicate submissions (POST/REDIRECT/GET pattern)
79
                print $input->redirect("/cgi-bin/koha/pos/registers.pl");
80
                exit;
81
            }
104
            }
105
        }
106
107
        if ( @errors && $success_count == 0 ) {
108
109
            # All failed - stay on page to show errors
110
            $template->param(
111
                error_cashup_start => 1,
112
                cashup_errors      => \@errors
113
            );
82
        } else {
114
        } else {
83
            $template->param( error_missing_register_start => 1 );
115
116
            # Some or all succeeded - redirect with coded parameters
117
            my $redirect_url = "/cgi-bin/koha/pos/registers.pl";
118
            my @params;
119
120
            if ( $success_count > 0 ) {
121
                push @params, "cashup_start_success=" . $success_count;
122
            }
123
            if (@errors) {
124
                push @params, "cashup_start_errors=" . scalar(@errors);
125
            }
126
127
            if (@params) {
128
                $redirect_url .= "?" . join( "&", @params );
129
            }
130
131
            print $input->redirect($redirect_url);
132
            exit;
84
        }
133
        }
85
    } else {
134
    } else {
86
        $template->param( error_cashup_permission => 1 );
135
        $template->param( error_cashup_permission => 1 );
87
    }
136
    }
88
} elsif ( $op eq 'cud-cashup' ) {
137
} elsif ( $op eq 'cud-cashup' ) {
89
    if ( $logged_in_user->has_permission( { cash_management => 'cashup' } ) ) {
138
    if ( $logged_in_user->has_permission( { cash_management => 'cashup' } ) ) {
90
        my $registerid = $input->param('registerid');
139
        my $registerid_param = $input->param('registerid');
91
        if ($registerid) {
140
        my @register_ids     = split( ',', $registerid_param );
92
            my $register = Koha::Cash::Registers->find( { id => $registerid } );
141
        my @errors           = ();
93
            $register->add_cashup(
142
        my $success_count    = 0;
94
                {
143
95
                    manager_id => $logged_in_user->id,
144
        foreach my $register_id (@register_ids) {
96
                    amount     => $register->outstanding_accountlines->total
145
            $register_id =~ s/^\s+|\s+$//g;    # Trim whitespace
97
                }
146
            next unless $register_id;
98
            );
147
99
        } else {
148
            my $register = Koha::Cash::Registers->find( { id => $register_id } );
100
            for my $register ( $registers->as_list ) {
149
            next unless $register;
150
151
            eval {
152
                # Quick cashup: use expected amount (no amount parameter = use expected)
101
                $register->add_cashup(
153
                $register->add_cashup(
102
                    {
154
                    {
103
                        manager_id => $logged_in_user->id,
155
                        manager_id => $logged_in_user->id,
104
                        amount     => $register->outstanding_accountlines->total
156
157
                        # No amount = uses expected amount from outstanding_accountlines
158
                        # No reconciliation_note = quick cashup assumes correct amounts
105
                    }
159
                    }
106
                );
160
                );
161
                $success_count++;
162
            };
163
            if ($@) {
164
                if ( $@->isa('Koha::Exceptions::Object::BadValue') ) {
165
                    push @errors, "Register " . $register->name . ": No cashup session to complete";
166
                } elsif ( $@->isa('Koha::Exceptions::Object::DuplicateID') ) {
167
                    push @errors, "Register " . $register->name . ": Cashup already completed";
168
                } else {
169
                    push @errors, "Register " . $register->name . ": Failed to complete cashup";
170
                }
107
            }
171
            }
108
        }
172
        }
109
173
110
        # Redirect to prevent duplicate submissions (POST/REDIRECT/GET pattern)
174
        if ( @errors && $success_count == 0 ) {
111
        print $input->redirect("/cgi-bin/koha/pos/registers.pl");
175
112
        exit;
176
            # All failed - stay on page to show errors
177
            $template->param(
178
                error_cashup_complete => 1,
179
                cashup_errors         => \@errors
180
            );
181
        } else {
182
183
            # Some or all succeeded - redirect with coded parameters
184
            my $redirect_url = "/cgi-bin/koha/pos/registers.pl";
185
            my @params;
186
187
            if ( $success_count > 0 ) {
188
                push @params, "cashup_complete_success=" . $success_count;
189
            }
190
            if (@errors) {
191
                push @params, "cashup_complete_errors=" . scalar(@errors);
192
            }
193
194
            if (@params) {
195
                $redirect_url .= "?" . join( "&", @params );
196
            }
197
198
            print $input->redirect($redirect_url);
199
            exit;
200
        }
113
    } else {
201
    } else {
114
        $template->param( error_cashup_permission => 1 );
202
        $template->param( error_cashup_permission => 1 );
115
    }
203
    }
116
- 

Return to bug 40445