Files
opensourcepos/app/Views/sales/form.php
objecttothis 9c542efaf6 Feature: Payment reference code (#4587)
* Add `reference_code` to sale payment queries and group by statements

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* Refactor `Sales` controller to improve payment handling readability and replace snake_case with camelCase variables

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* Add missing translations for `Sales` language file and include new keys like `must_enter_rrn` and `reference_code`

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* Refactor `Sales` payment handling to use camelCase and extend `addPayment` with `referenceCode` support

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* Refactor `Sales` models, controllers, and libraries to adopt camelCase naming conventions and improve readability

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* Add translations and updates for `must_enter_reference_code` and `reference_code` across language files and update `Sales` controller to replace `must_enter_rrn` with the new key

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* feat(sales): add reference code input and payment type helper

- Add `get_reference_code_payment_types()` to locale_helper as single
  source of truth for card-requiring payment types
- Add reference code row to register view, shown/hidden via JS based
  on selected payment type
- Fix payment type dropdown width to 100% for consistent layout
- Add min-width to payment buttons and right-padding to button group

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* feat(config): add payment reference code length configuration

- Add payment_reference_code_min and payment_reference_code_max fields
  to Config controller save logic
- Add translation keys for reference code length limits across all
  language files (min/max label + section header)
- Align array key formatting in Config controller for readability

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* style(lang): normalize string delimiters to single quotes across all language files

Convert double-quoted array keys and values to single quotes in all
app/Language/*/Config.php and app/Language/*/Sales.php variants.
No translation content changed — formatting only.

Also add Localization section to AGENTS.md documenting language file
conventions for new keys and fallback behavior.

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* feat(lang): add payment reference code length translations

Add localized strings for payment_reference_code_length_limits,
payment_reference_code_length_max_label, and
payment_reference_code_length_min_label across all supported locales.

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* test(config,sales): add payment reference code min/max validation tests

- Add baseLocalePayload() helper in ConfigTest for postSaveLocale tests
- Add testSaveLocale_AcceptsValidReferenceCodeMinMax and related boundary tests
- Add Sale_libPaymentTest for payment reference code validation in Sale_lib

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* fix(sales): add type-specific validation for amount_tendered

Gift card payments use amount_tendered as giftcard number (integer);
cash/other payments require decimal_locale format. Apply correct
validation rule per payment type instead of generic required.

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* fix(lang): replace self-closing </br> with <br> in all locales

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* fix(sales): use configurable precision in discount comparison

Replace hardcoded precision 2 with totals_decimals() when comparing
discount against item total via bccomp/bcmul, so discount validation
respects the configured decimal precision setting.

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* fix(lang): correct Azerbaijani translations in Config.php

Replace placeholder/mismatched strings with accurate translations:
- email_mailpath, email_smtp_pass, invoice_email_message
- number_locale_invalid/required, receipt_template
- reward_configuration, right, tax_decimals, theme

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* feat(sales): add reference code support to payment edit flow

- Add reference_code field to new payment row in sale edit form
- Persist reference_code on insert in Sale model
- Validate reference_code_new in Sales controller using configurable min/max length rules

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* feat(lang): add Georgian (ka) language stubs for Config and Sales

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* Match the fallback maximum reference_code length to the maximum of the field in the db

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* Bug fixes

- Add validation of UI settings to prevent overridden values from being passed.
- Correct maximum value in JS for payment_reference_code maximum length to 40.
- Fix bug causing copy_entire_sale() to incorrectly copy the reference code and cash_adjustment

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

---------

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>
Co-authored-by: Travis Garrison <travis@chiraqbookstore.com>
2026-07-10 13:49:05 +04:00

277 lines
14 KiB
PHP

<?php
/**
* @var array $sale_info
* @var bool $balance_due
* @var array $new_payment_options
* @var string $payment_type_new
* @var float $payment_amount_new
* @var array $payments
* @var array $payment_amounts
* @var array $payment_options
* @var string $selected_customer_name
* @var int $selected_customer_id
* @var string $selected_employee_name
* @var int $selected_employee_id
* @var string $controller_name
* @var array $config
*/
?>
<div id="required_fields_message"><?= lang('Common.fields_required_message') ?></div>
<ul id="error_message_box" class="error_message_box"></ul>
<?= form_open('sales/save/' . $sale_info['sale_id'], ['id' => 'sales_edit_form', 'class' => 'form-horizontal']) ?>
<fieldset id="sale_basic_info">
<div class="form-group form-group-sm">
<?= form_label(lang('Sales.receipt_number'), 'receipt_number', ['class' => 'control-label col-xs-3']) ?>
<?= anchor('sales/receipt/' . $sale_info['sale_id'], 'POS ' . $sale_info['sale_id'], ['target' => '_blank', 'class' => 'control-label col-xs-8', "style" => "text-align: left"]) ?>
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('Sales.date'), 'date', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-8">
<?= form_input(['name' => 'date', 'value' => to_datetime(strtotime($sale_info['sale_time'])), 'class' => 'datetime form-control input-sm']) ?>
</div>
</div>
<?php if ($config['invoice_enable']) { ?>
<div class="form-group form-group-sm">
<?= form_label(lang('Sales.invoice_number'), 'invoice_number', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-8">
<?php if (!empty($sale_info["invoice_number"]) && isset($sale_info['customer_id']) && !empty($sale_info['email'])): ?>
<?= form_input(['name' => 'invoice_number', 'size' => 10, 'value' => $sale_info['invoice_number'], 'id' => 'invoice_number', 'class' => 'form-control input-sm']) ?>
<a id="send_invoice" href="javascript:void(0);"><?= lang('Sales.send_invoice') ?></a>
<?php else: ?>
<?= form_input(['name' => 'invoice_number', 'value' => $sale_info['invoice_number'], 'id' => 'invoice_number', 'class' => 'form-control input-sm']) ?>
<?php endif; ?>
</div>
</div>
<?php } ?>
<?php if ($balance_due) { ?>
<div class="form-group form-group-sm">
<?= form_label(lang('Sales.payment'), 'payment_new', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-4">
<?= form_dropdown('payment_type_new', $new_payment_options, $payment_type_new, ['id' => 'payment_types_new', 'class' => 'form-control']) ?>
</div>
<div class="col-xs-4">
<div class="input-group input-group-sm">
<?php if (!is_right_side_currency_symbol()): ?>
<span class="input-group-addon input-sm"><b><?= esc($config['currency_symbol']) ?></b></span>
<?php endif; ?>
<?= form_input(['name' => 'payment_amount_new', 'value' => $payment_amount_new, 'id' => 'payment_amount_new', 'class' => 'form-control input-sm']) // TODO: potentially we need to add type to be float/decimal/numeric to reduce improper data entry ?>
<?php if (is_right_side_currency_symbol()): ?>
<span class="input-group-addon input-sm"><b><?= esc($config['currency_symbol']) ?></b></span>
<?php endif; ?>
</div>
</div>
</div>
<div class="form-group form-group-sm reference-code-input-new" style="display:none;">
<?= form_label(lang('Sales.reference_code'), 'reference_code_new', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-8">
<?= form_input(['name' => 'reference_code_new', 'id' => 'reference_code_new', 'value' => '', 'class' => 'form-control input-sm', 'disabled' => true]) ?>
</div>
</div>
<?php } ?>
<?php
$i = 0;
foreach ($payments as $row) {
?>
<div class="form-group form-group-sm">
<?= form_label(lang('Sales.payment'), "payment_$i", ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-4">
<?php // No editing of Gift Card payments as it's a complex change ?>
<?= form_hidden("payment_id_$i", $row->payment_id) ?>
<?= form_hidden("reference_code_$i", $row->reference_code ?? '') ?>
<?php if (!empty(strstr($row->payment_type, lang('Sales.giftcard')))): ?>
<?= form_input(['name' => "payment_type_$i", 'value' => $row->payment_type, 'id' => "payment_type_$i", 'class' => 'form-control input-sm', 'readonly' => 'true']) ?>
<?php else: ?>
<?= form_dropdown("payment_type_$i", $payment_options, $row->payment_type, ['id' => "payment_types_$i", 'class' => 'form-control']) ?>
<?php endif; ?>
</div>
<div class="col-xs-4">
<div class="input-group input-group-sm">
<?php if (!is_right_side_currency_symbol()): ?>
<span class="input-group-addon input-sm"><b><?= esc($config['currency_symbol']) ?></b></span>
<?php endif; ?>
<?= form_input(['name' => "payment_amount_$i", 'value' => to_currency_no_money($row->payment_amount), 'id' => "payment_amount_$i", 'class' => 'form-control input-sm', 'readonly' => 'true']) // TODO: add type attribute ?>
<?php if (is_right_side_currency_symbol()): ?>
<span class="input-group-addon input-sm"><b><?= esc($config['currency_symbol']) ?></b></span>
<?php endif; ?>
</div>
</div>
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('Sales.refund'), "refund_$i", ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-4">
<?php // No editing of Gift Card payments as it's a complex change ?>
<?php if (!empty(strstr($row->payment_type, lang('Sales.giftcard')))): ?>
<?= form_input(['name' => "refund_type_$i", 'value' => lang('Sales.cash'), 'id' => "refund_type_$i", 'class' => 'form-control input-sm', 'readonly' => 'true']) ?>
<?php else: ?>
<?= form_dropdown("refund_type_$i", $payment_options, lang('Sales.cash'), ['id' => "refund_types_$i", 'class' => 'form-control']) ?>
<?php endif; ?>
</div>
<div class="col-xs-4">
<div class="input-group input-group-sm">
<?php if (!is_right_side_currency_symbol()): ?>
<span class="input-group-addon input-sm"><b><?= esc($config['currency_symbol']) ?></b></span>
<?php endif; ?>
<?= form_input(['name' => "refund_amount_$i", 'value' => to_currency_no_money($row->cash_refund), 'id' => "refund_amount_$i", 'class' => 'form-control input-sm', 'readonly' => 'true']) ?>
<?php if (is_right_side_currency_symbol()): ?>
<span class="input-group-addon input-sm"><b><?= esc($config['currency_symbol']) ?></b></span>
<?php endif; ?>
</div>
</div>
</div>
<?php
++$i;
}
echo form_hidden('number_of_payments', strval($i));
?>
<div class="form-group form-group-sm">
<?= form_label(lang('Sales.customer'), 'customer', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-8">
<?= form_input(['name' => 'customer_name', 'value' => $selected_customer_name, 'id' => 'customer_name', 'class' => 'form-control input-sm']) ?>
<?= form_hidden('customer_id', $selected_customer_id ?? '') ?>
</div>
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('Sales.employee'), 'employee', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-8">
<?= form_input(['name' => 'employee_name', 'value' => $selected_employee_name, 'id' => 'employee_name', 'class' => 'form-control input-sm']) ?>
<?= form_hidden('employee_id', $selected_employee_id) ?>
</div>
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('Sales.comment'), 'comment', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-8">
<?= form_textarea(['name' => 'comment', 'value' => $sale_info['comment'], 'id' => 'comment', 'class' => 'form-control input-sm']) ?>
</div>
</div>
</fieldset>
<?= form_close() ?>
<script type="text/javascript">
$(document).ready(function() {
<?php if (!empty($sale_info['email'])): ?>
$('#send_invoice').click(function(event) {
if (confirm("<?= lang('Sales.invoice_confirm') . ' ' . $sale_info['email'] ?>")) {
$.get("<?= esc("$controller_name/sendPdf/" . $sale_info['sale_id']) ?>",
function(response) {
BootstrapDialog.closeAll();
$.notify({
message: response.message
}, {
type: response.success ? 'success' : 'danger'
})
}, 'json'
);
}
});
<?php endif; ?>
<?= view('partial/datepicker_locale') ?>
var fill_value_customer = function(event, ui) {
event.preventDefault();
$("input[name='customer_id']").val(ui.item.value);
$("input[name='customer_name']").val(ui.item.label);
};
$('#customer_name').autocomplete({
source: "<?= 'customers/suggest' ?>",
minChars: 0,
delay: 15,
cacheLength: 1,
appendTo: '.modal-content',
select: fill_value_customer,
focus: fill_value_customer
});
var fill_value_employee = function(event, ui) {
event.preventDefault();
$("input[name='employee_id']").val(ui.item.value);
$("input[name='employee_name']").val(ui.item.label);
};
$('#employee_name').autocomplete({
source: "<?= 'employees/suggest' ?>",
minChars: 0,
delay: 15,
cacheLength: 1,
appendTo: '.modal-content',
select: fill_value_employee,
focus: fill_value_employee
});
const referenceCodePaymentTypes = <?= json_encode($reference_code_payment_types ?? []) ?>;
$('#payment_types_new').on('change', function() {
const paymentType = $(this).val();
const needsReferenceCode = referenceCodePaymentTypes.indexOf(paymentType) !== -1;
if (needsReferenceCode) {
$('.reference-code-input-new').show();
$('#reference_code_new').attr('disabled', false);
} else {
$('.reference-code-input-new').hide();
$('#reference_code_new').attr('disabled', true).val('');
}
});
$('button#delete').click(function() {
dialog_support.hide();
table_support.do_delete("<?= esc($controller_name); ?>", <?= $sale_info['sale_id'] ?>);
});
$('button#restore').click(function() {
dialog_support.hide();
table_support.do_restore("<?= esc($controller_name) ?>", <?= $sale_info['sale_id'] ?>);
});
$('#sales_edit_form').validate($.extend({
submitHandler: function(form) {
$(form).ajaxSubmit({
success: function(response) {
dialog_support.hide();
table_support.handle_submit("<?= esc($controller_name) ?>", response);
const params = $.param(table_support.query_params());
$.get("<?= $controller_name; ?>/search?" + params, function(response) {
$("#payment_summary").html(response.payment_summary);
}, 'json');
},
dataType: 'json'
});
},
errorLabelContainer: '#error_message_box',
rules: {
invoice_number: {
remote: {
url: "<?= esc("$controller_name/checkInvoiceNumber") ?>",
type: 'POST',
data: {
'sale_id': <?= $sale_info['sale_id'] ?>,
'invoice_number': function() {
return $('#invoice_number').val();
}
}
}
}
},
messages: {
invoice_number: "<?= lang('Sales.invoice_number_duplicate') ?>"
}
}, form_support.error));
});
</script>