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

(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/refund_modal.inc (+90 lines)
Line 0 Link Here
1
<!-- Issue refund modal -->
2
<div class="modal" id="issueRefundModal" tabindex="-1" role="dialog" aria-labelledby="issueRefundLabel">
3
    <form id="refund_form" action="[% refund_form_action | html %]" method="post" enctype="multipart/form-data" class="validated">
4
        [% INCLUDE 'csrf-token.inc' %]
5
        <input type="hidden" name="accountline_id" value="" id="refundline" />
6
        <input type="hidden" name="op" value="cud-refund" />
7
        [% IF patron %]
8
            <input type="hidden" name="borrowernumber" value="[% patron.borrowernumber | html %]" />
9
        [% END %]
10
        [% IF register_id %]
11
            <input type="hidden" name="registerid" value="[% register_id | html %]" id="refund_registerid" />
12
        [% ELSE %]
13
            <input type="hidden" name="registerid" value="" id="refund_registerid" />
14
        [% END %]
15
        <div class="modal-dialog">
16
            <div class="modal-content">
17
                <div class="modal-header">
18
                    <h1 class="modal-title" id="issueRefundLabel">Issue refund</h1>
19
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
20
                </div>
21
                <div class="modal-body">
22
                    <div id="refund_warning" class="alert alert-warning" style="display: none;"></div>
23
24
                    [% IF Koha.Preference('UseCashRegisters') && ( registers.size == 0 ) %]
25
                        <div id="refund_error" class="alert alert-warning"> You must have at least one cash register associated with this branch before you can record cash refunds. </div>
26
                    [% END %]
27
28
                    <!-- Debit summary -->
29
                    <div id="refund_debit_summary" class="alert alert-info" style="margin-bottom: 20px;">
30
                        <h4>Transaction details</h4>
31
                        <table class="table table-sm">
32
                            <tr>
33
                                <th>Description:</th>
34
                                <td id="refund_debit_description"></td>
35
                            </tr>
36
                            <tr>
37
                                <th>Original charge:</th>
38
                                <td id="refund_debit_amount"></td>
39
                            </tr>
40
                            <tr>
41
                                <th>Amount paid:</th>
42
                                <td id="refund_amount_paid"></td>
43
                            </tr>
44
                            <tr id="refund_payment_method_row" style="display: none;">
45
                                <th>Payment method:</th>
46
                                <td id="refund_payment_method"></td>
47
                            </tr>
48
                            <tr id="refund_original_register_row" style="display: none;">
49
                                <th>Original register:</th>
50
                                <td id="refund_original_register"></td>
51
                            </tr>
52
                        </table>
53
                    </div>
54
55
                    <fieldset class="rows">
56
                        <ol>
57
                            <li>
58
                                <label class="required" for="refund_amount">Amount to refund: </label>
59
                                <input type="text" inputmode="decimal" pattern="^\d+(\.\d{2})?$" id="refund_amount" name="amount" required="required" />
60
                                <span class="required">Required</span>
61
                            </li>
62
63
                            [% INCLUDE 'transaction_types.inc' type="refund" %]
64
65
                            [% IF Koha.Preference('UseCashRegisters') && !register_id %]
66
                                <li>
67
                                    <label for="refund_registerid_select">Cash register: </label>
68
                                    <select name="registerid" id="refund_registerid_select">
69
                                        <option id="noregister" disabled selected="selected" value="">-- Select an option --</option>
70
                                        [% PROCESS options_for_registers %]
71
                                    </select>
72
                                </li>
73
                            [% END %]
74
75
                            <li>
76
                                <label for="refund_note">Note: </label>
77
                                <textarea id="refund_note" name="refund_note" rows="5" cols="30"></textarea>
78
                            </li>
79
                        </ol>
80
                    </fieldset>
81
                </div>
82
                <div class="modal-footer">
83
                    <button type="submit" class="btn btn-primary" id="refund_confirm">Confirm</button>
84
                    <button type="button" class="btn btn-default" data-bs-dismiss="modal">Cancel</button>
85
                </div>
86
            </div>
87
        </div>
88
    </form>
89
</div>
90
<!-- /#issueRefundModal -->
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/pos/pay.tt (+236 lines)
Lines 1-5 Link Here
1
[% USE raw %]
1
[% USE raw %]
2
[% USE AdditionalContents %]
2
[% USE AdditionalContents %]
3
[% USE AuthorisedValues %]
3
[% USE Asset %]
4
[% USE Asset %]
4
[% USE Branches %]
5
[% USE Branches %]
5
[% USE Koha %]
6
[% USE Koha %]
Lines 7-12 Link Here
7
[% USE TablesSettings %]
8
[% USE TablesSettings %]
8
[% USE Registers %]
9
[% USE Registers %]
9
[% PROCESS 'i18n.inc' %]
10
[% PROCESS 'i18n.inc' %]
11
[% PROCESS 'html_helpers.inc' %]
10
[% SET footerjs = 1 %]
12
[% SET footerjs = 1 %]
11
[% INCLUDE 'doc-head-open.inc' %]
13
[% INCLUDE 'doc-head-open.inc' %]
12
[% SET registers = Registers.all( filters => { current_branch => 1 } ) %]
14
[% SET registers = Registers.all( filters => { current_branch => 1 } ) %]
Lines 33-38 Link Here
33
[% END #/ WRAPPER sub-header.inc %]
35
[% END #/ WRAPPER sub-header.inc %]
34
36
35
[% WRAPPER 'main-container.inc' aside='pos-menu' %]
37
[% WRAPPER 'main-container.inc' aside='pos-menu' %]
38
39
    [% IF ( CAN_user_cash_management_anonymous_refund ) %]
40
        <div id="toolbar" class="btn-toolbar">
41
            <button id="process_refund" type="button" class="btn btn-default" data-bs-toggle="modal" data-bs-target="#refundSearchModal"><i class="fa-solid fa-undo"></i> Process refund</button>
42
        </div>
43
    [% END %]
44
36
    <h1>Point of sale</h1>
45
    <h1>Point of sale</h1>
37
46
38
    [% IF ( registers.size == 0 ) %]
47
    [% IF ( registers.size == 0 ) %]
Lines 240-245 Link Here
240
    </div>
249
    </div>
241
</div>
250
</div>
242
251
252
<!-- Refund search modal -->
253
<div class="modal" id="refundSearchModal" tabindex="-1" role="dialog" aria-labelledby="refundSearchLabel">
254
    <div class="modal-dialog modal-lg">
255
        <div class="modal-content">
256
            <div class="modal-header">
257
                <h1 class="modal-title" id="refundSearchLabel">Search transaction for refund</h1>
258
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
259
            </div>
260
            <div class="modal-body">
261
                <!-- Messages block for errors/warnings -->
262
                <div id="refund_search_messages"></div>
263
264
                <!-- Search form -->
265
                <div id="refund_search_form">
266
                    <div class="input-group">
267
                        <input type="text" id="receipt_id" name="receipt_id" class="form-control" placeholder="Enter receipt number or transaction ID" />
268
                        <button type="button" class="btn btn-primary" id="search_transaction"><i class="fa fa-search"></i> Search</button>
269
                    </div>
270
                </div>
271
272
                <!-- Search results -->
273
                <div id="refund_search_results" style="display: none; margin-top: 20px;">
274
                    <div class="page-section">
275
                        <div id="transaction_info"></div>
276
                        <div id="transaction_debits" style="margin-top: 15px;">
277
                            <h5>Items in this transaction:</h5>
278
                            <table id="search_results_table" class="table table-bordered table-striped">
279
                                <thead>
280
                                    <tr>
281
                                        <th>Description</th>
282
                                        <th>Amount</th>
283
                                        <th>Status</th>
284
                                        <th class="NoSort">Action</th>
285
                                    </tr>
286
                                </thead>
287
                                <tbody></tbody>
288
                            </table>
289
                        </div>
290
                    </div>
291
                </div>
292
            </div>
293
            <div class="modal-footer">
294
                <button type="button" class="btn btn-default" data-bs-dismiss="modal">Close</button>
295
            </div>
296
        </div>
297
    </div>
298
</div>
299
<!-- /#refundSearchModal -->
300
301
[% INCLUDE 'modals/refund_modal.inc' refund_form_action = '/cgi-bin/koha/pos/register.pl' registers = registers %]
302
243
[% IF payment_id && Koha.Preference('FinePaymentAutoPopup') %]
303
[% IF payment_id && Koha.Preference('FinePaymentAutoPopup') %]
244
    <!-- Automatic Print Receipt -->
304
    <!-- Automatic Print Receipt -->
245
    <a id="printReceipt" style="display: none" href="#"></a>
305
    <a id="printReceipt" style="display: none" href="#"></a>
Lines 247-254 Link Here
247
307
248
[% MACRO jsinclude BLOCK %]
308
[% MACRO jsinclude BLOCK %]
249
    [% INCLUDE 'format_price.inc' %]
309
    [% INCLUDE 'format_price.inc' %]
310
    [% INCLUDE 'js-date-format.inc' %]
250
    [% INCLUDE 'datatables.inc' %]
311
    [% INCLUDE 'datatables.inc' %]
312
    [% Asset.js("js/refund_modal.js") | $raw %]
251
    <script>
313
    <script>
314
        // Create lookup objects for descriptions
315
        var debitTypeDescriptions = {
316
            [% FOREACH invoice_type IN invoice_types %]
317
                '[% invoice_type.code | html %]': '[% invoice_type.description | html %]',
318
            [% END %]
319
        };
320
321
        // Create lookup for payment type descriptions
322
        var paymentTypeDescriptions = {
323
            [% FOREACH pt IN AuthorisedValues.GetAuthValueDropbox('PAYMENT_TYPE') %]
324
                '[% pt.authorised_value | html %]': '[% pt.lib | html %]',
325
            [% END %]
326
        };
327
252
        function moneyFormat(textObj) {
328
        function moneyFormat(textObj) {
253
            var newValue = textObj.value;
329
            var newValue = textObj.value;
254
            var decAmount = "";
330
            var decAmount = "";
Lines 527-532 Link Here
527
                });
603
                });
528
                $("#printReceipt").click();
604
                $("#printReceipt").click();
529
            [% END %]
605
            [% END %]
606
607
            // Refund search functionality
608
            var current_registerid = $('select[name="registerid"]').val();
609
610
            // Reset search modal when opened
611
            $('#refundSearchModal').on('show.bs.modal', function() {
612
                $('#receipt_id').val('');
613
                $('#refund_search_results').hide();
614
                $('#refund_search_messages').empty();
615
                $('#search_results_table tbody').empty();
616
                $('#transaction_info').empty();
617
            });
618
619
            // Auto-focus search input when modal is fully shown
620
            $('#refundSearchModal').on('shown.bs.modal', function() {
621
                $('#receipt_id').focus();
622
            });
623
624
            // Search transaction button click
625
            $('#search_transaction').on('click', function() {
626
                var receipt_id = $('#receipt_id').val().trim();
627
628
                if (!receipt_id) {
629
                    showSearchMessage('Please enter a receipt number or transaction ID.', 'warning');
630
                    return;
631
                }
632
633
                $('#refund_search_messages').empty();
634
                $('#refund_search_results').hide();
635
636
                // Build query - search by both credit_number and accountlines_id
637
                var query = JSON.stringify({
638
                    "-or": [
639
                        { "credit_number": receipt_id },
640
                        { "accountlines_id": receipt_id }
641
                    ]
642
                });
643
644
                // Call API to search for credit
645
                $.ajax({
646
                    url: '/api/v1/account/credits',
647
                    method: 'GET',
648
                    data: {
649
                        q: query,
650
                    },
651
                    headers: {
652
                        'x-koha-embed': 'debits'
653
                    },
654
                    success: function(data) {
655
                        if (data && data.length > 0) {
656
                            displayTransactionResults(data[0]);
657
                        } else {
658
                            showSearchMessage('No transaction found with that receipt number or ID.', 'warning');
659
                        }
660
                    },
661
                    error: function(xhr) {
662
                        var errorMsg = 'Error searching for transaction.';
663
                        if (xhr.responseJSON && xhr.responseJSON.error) {
664
                            errorMsg = xhr.responseJSON.error;
665
                        }
666
                        showSearchMessage(errorMsg, 'danger');
667
                    }
668
                });
669
            });
670
671
            // Allow Enter key to trigger search
672
            $('#receipt_id').on('keypress', function(e) {
673
                if (e.which === 13) {
674
                    e.preventDefault();
675
                    $('#search_transaction').click();
676
                }
677
            });
678
679
            function showSearchMessage(message, type) {
680
                var alertClass = 'alert-' + type;
681
                var iconClass = type === 'danger' ? 'fa-exclamation-circle' : 'fa-exclamation-triangle';
682
                var html = '<div class="alert ' + alertClass + '" role="alert">';
683
                html += '<i class="fa ' + iconClass + '"></i> ';
684
                html += message;
685
                html += '</div>';
686
                $('#refund_search_messages').html(html);
687
            }
688
689
            function displayTransactionResults(credit) {
690
                // Get payment type description for display
691
                var paymentTypeDesc = paymentTypeDescriptions[credit.payment_type] || credit.payment_type || '';
692
                var info_html = '<h4>Transaction details</h4>';
693
                info_html += '<table class="table table-sm">';
694
                info_html += '<tr><th>Receipt #:</th><td>' + (credit.credit_number || credit.account_line_id) + '</td></tr>';
695
                info_html += '<tr><th>Date:</th><td>' + $date(credit.date) + '</td></tr>';
696
                info_html += '<tr><th>Total:</th><td>' + Math.abs(parseFloat(credit.amount)).format_price() + '</td></tr>';
697
                info_html += '<tr><th>Payment type:</th><td>' + paymentTypeDesc + '</td></tr>';
698
                info_html += '</table>';
699
700
                // Show warning if from different register
701
                if (credit.cash_register_id && credit.cash_register_id != current_registerid) {
702
                    showSearchMessage('Note: This transaction was made at a different cash register.', 'warning');
703
                }
704
705
                $('#transaction_info').html(info_html);
706
707
                // Display debits
708
                var tbody = $('#search_results_table tbody');
709
                tbody.empty();
710
711
                if (credit.debits && credit.debits.length > 0) {
712
                    credit.debits.forEach(function(debit) {
713
                        // Don't show PAYOUT debits or already REFUNDED debits
714
                        if (debit.type === 'PAYOUT' || debit.status === 'REFUNDED') {
715
                            return;
716
                        }
717
718
                        // Get description from lookup, fallback to debit.description, then type code
719
                        var debitDesc = debitTypeDescriptions[debit.type] || debit.description || debit.type || 'Item';
720
721
                        var row = '<tr>';
722
                        row += '<td>' + debitDesc + '</td>';
723
                        row += '<td>' + parseFloat(debit.amount).format_price() + '</td>';
724
                        row += '<td>' + (debit.status || 'Paid') + '</td>';
725
                        row += '<td>';
726
727
                        if (debit.status !== 'REFUNDED') {
728
729
                            row += '<button type="button" class="btn btn-sm btn-default refund_debit_btn" ';
730
                            row += 'data-bs-toggle="modal" ';
731
                            row += 'data-bs-target="#issueRefundModal" ';
732
                            row += 'data-accountline="' + debit.account_line_id + '" ';
733
                            row += 'data-amount="' + debit.amount + '" ';
734
                            row += 'data-amountoutstanding="' + (debit.amount_outstanding || 0) + '" ';
735
                            row += 'data-item="' + debitDesc + '" ';
736
                            row += 'data-register-id="' + (credit.cash_register_id || '') + '" ';
737
                            row += 'data-payment-type="' + paymentTypeDesc + '" ';
738
                            row += 'data-member="">';
739
                            row += '<i class="fa-solid fa-money-bill-1"></i> Issue refund</button>';
740
                        } else {
741
                            row += '<span class="badge badge-secondary">Already refunded</span>';
742
                        }
743
744
                        row += '</td>';
745
                        row += '</tr>';
746
747
                        tbody.append(row);
748
                    });
749
750
                    $('#refund_search_results').show();
751
                } else {
752
                    showSearchMessage('No refundable items found in this transaction.', 'info');
753
                }
754
            }
755
756
            // Handle "Issue refund" button click in search results
757
            // Close search modal when refund button is clicked
758
            $(document).on('click', '.refund_debit_btn', function() {
759
                $('#refundSearchModal').modal('hide');
760
            });
761
762
            // Initialize refund modal with current register context
763
            initRefundModal({
764
                currentRegisterId: current_registerid
765
            });
530
        });
766
        });
531
    </script>
767
    </script>
532
[% END %]
768
[% END %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/pos/register.tt (-1 / +5 lines)
Lines 52-57 Link Here
52
            <div id="error_message" class="alert alert-warning"> You do not have permission to perform refund actions. </div>
52
            <div id="error_message" class="alert alert-warning"> You do not have permission to perform refund actions. </div>
53
        [% END %]
53
        [% END %]
54
54
55
        [% IF ( error_refund_not_found ) %]
56
            <div id="error_message" class="alert alert-warning"> The account line you are trying to refund was not found. </div>
57
        [% END %]
58
55
        [% IF ( CAN_user_cash_management_cashup ) %]
59
        [% IF ( CAN_user_cash_management_cashup ) %]
56
            <div id="toolbar" class="btn-toolbar">
60
            <div id="toolbar" class="btn-toolbar">
57
                <button id="pos_cashup" type="button" class="btn btn-default" data-bs-toggle="modal" data-bs-target="#confirmCashupModal"><i class="fa-solid fa-money-bill-1"></i> Record cashup</button>
61
                <button id="pos_cashup" type="button" class="btn btn-default" data-bs-toggle="modal" data-bs-target="#confirmCashupModal"><i class="fa-solid fa-money-bill-1"></i> Record cashup</button>
Lines 397-403 Link Here
397
<div class="modal" id="issueRefundModal" tabindex="-1" role="dialog" aria-labelledby="issueRefundLabel">
401
<div class="modal" id="issueRefundModal" tabindex="-1" role="dialog" aria-labelledby="issueRefundLabel">
398
    <form id="refund_form" method="post" enctype="multipart/form-data" class="validated">
402
    <form id="refund_form" method="post" enctype="multipart/form-data" class="validated">
399
        [% INCLUDE 'csrf-token.inc' %]
403
        [% INCLUDE 'csrf-token.inc' %]
400
        <input type="hidden" name="accountline" value="" id="refundline" />
404
        <input type="hidden" name="accountline_id" value="" id="refundline" />
401
        <div class="modal-dialog">
405
        <div class="modal-dialog">
402
            <div class="modal-content">
406
            <div class="modal-content">
403
                <div class="modal-header">
407
                <div class="modal-header">
(-)a/koha-tmpl/intranet-tmpl/prog/js/refund_modal.js (+129 lines)
Line 0 Link Here
1
/**
2
 * Shared refund modal functionality
3
 * Handles the issue refund modal across POS and patron account pages
4
 */
5
6
(function () {
7
    "use strict";
8
9
    /**
10
     * Initialize refund modal event handlers
11
     * @param {Object} options - Configuration options
12
     * @param {boolean} options.validateCashRegister - Whether to validate cash register selection
13
     * @param {string} options.currentRegisterId - Current register ID (for POS pages)
14
     */
15
    window.initRefundModal = function (options) {
16
        options = options || {};
17
        var validateCashRegister = options.validateCashRegister || false;
18
        var currentRegisterId = options.currentRegisterId || null;
19
20
        /**
21
         * Handle modal shown event
22
         * Populate modal fields with data from the trigger button
23
         */
24
        $("#issueRefundModal").on("shown.bs.modal", function (e) {
25
            var button = $(e.relatedTarget);
26
27
            // Get data from button attributes
28
            var item = button.data("item");
29
            var accountline = button.data("accountline");
30
            var amount = parseFloat(button.data("amount"));
31
            var amountoutstanding =
32
                parseFloat(button.data("amountoutstanding")) || 0;
33
            var paymentType = button.data("payment-type");
34
            var member = button.data("member");
35
            var registerId = button.data("register-id");
36
37
            // Calculate amount paid
38
            var paid = amount - amountoutstanding;
39
40
            // Populate transaction details section
41
            $("#refund_debit_description").text(item);
42
            $("#refund_debit_amount").text(amount.format_price());
43
            $("#refund_amount_paid").text(paid.format_price());
44
45
            // Show payment method if available
46
            if (paymentType) {
47
                $("#refund_payment_method").text(paymentType);
48
                $("#refund_payment_method_row").show();
49
            } else {
50
                $("#refund_payment_method_row").hide();
51
            }
52
53
            // Show original register info if different from current (POS context)
54
            if (
55
                registerId &&
56
                currentRegisterId &&
57
                registerId != currentRegisterId
58
            ) {
59
                $("#refund_original_register").text("Register #" + registerId);
60
                $("#refund_original_register_row").show();
61
                $("#refund_warning")
62
                    .html(
63
                        "<strong>Note:</strong> Original payment was made at a different register. " +
64
                            "The refund will be processed from your current register."
65
                    )
66
                    .show();
67
            } else if ($("#refund_original_register_row").length) {
68
                $("#refund_original_register_row").hide();
69
                $("#refund_warning").hide();
70
            }
71
72
            // Populate form fields
73
            $("#refundline").val(accountline);
74
            $("#refund_amount").attr({
75
                value: paid.format_price(),
76
                max: paid.format_price(),
77
                min: 0,
78
            });
79
80
            // Set register ID if provided
81
            if (registerId && $("#refund_registerid").length) {
82
                $("#refund_registerid").val(registerId);
83
            } else if (currentRegisterId && $("#refund_registerid").length) {
84
                $("#refund_registerid").val(currentRegisterId);
85
            }
86
87
            // Handle account credit option based on member
88
            if (member === "" || member === undefined) {
89
                $("#refund_type option[value='AC']").remove();
90
            } else if ($("#refund_type option[value='AC']").length == 0) {
91
                $("#refund_type").prepend(
92
                    '<option value="AC" selected="selected">Account credit</option>'
93
                );
94
            }
95
96
            // Clear note field
97
            $("#refund_note").val("");
98
99
            // Focus on amount and type fields
100
            $("#refund_amount, #refund_type").focus();
101
        });
102
103
        /**
104
         * Validate cash register requirement for refunds
105
         * Only applies when validateCashRegister is true
106
         */
107
        if (validateCashRegister) {
108
            $("#refund_form").on("submit", function (e) {
109
                var refund_type = $("#refund_type").val();
110
                var register = $("#refund_registerid_select").val();
111
112
                // Only require register for non-AC refunds with CASH or SIP00 payment types
113
                if (
114
                    refund_type &&
115
                    refund_type !== "AC" &&
116
                    (refund_type === "CASH" || refund_type.match(/^SIP\d{2}$/))
117
                ) {
118
                    if (!register || register === "") {
119
                        e.preventDefault();
120
                        alert(__("Cash register is required for cash refunds"));
121
                        $("#refund_registerid_select").focus();
122
                        return false;
123
                    }
124
                }
125
                return true;
126
            });
127
        }
128
    };
129
})();
(-)a/pos/register.pl (-2 / +7 lines)
Lines 121-130 if ( !$registers->count ) { Link Here
121
        if ( $logged_in_user->has_permission( { cash_management => 'anonymous_refund' } ) ) {
121
        if ( $logged_in_user->has_permission( { cash_management => 'anonymous_refund' } ) ) {
122
            my $amount         = $input->param('amount');
122
            my $amount         = $input->param('amount');
123
            my $quantity       = $input->param('quantity');
123
            my $quantity       = $input->param('quantity');
124
            my $accountline_id = $input->param('accountline');
124
            my $accountline_id = $input->param('accountline_id');
125
            my $refund_type    = $input->param('refund_type');
125
            my $refund_type    = $input->param('refund_type');
126
126
127
            my $accountline = Koha::Account::Lines->find($accountline_id);
127
            my $accountline = Koha::Account::Lines->find($accountline_id);
128
129
            unless ($accountline) {
130
                $template->param( error_refund_not_found => 1 );
131
                output_html_with_http_headers( $input, $cookie, $template->output );
132
                exit;
133
            }
128
            $schema->txn_do(
134
            $schema->txn_do(
129
                sub {
135
                sub {
130
136
131
- 

Return to bug 41592