mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-26 01:05:07 -04:00
Compare commits
2 Commits
master
...
fix-tax-in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6633bb36a8 | ||
|
|
30d5ac4496 |
@@ -808,10 +808,14 @@ class Config extends Secure_Controller
|
||||
$default_tax_1_rate = $this->request->getPost('default_tax_1_rate');
|
||||
$default_tax_2_rate = $this->request->getPost('default_tax_2_rate');
|
||||
|
||||
// Note: parse_tax() is not used here because these inputs use type="number"
|
||||
// which always submits dot-decimal values regardless of locale. Using parse_tax()
|
||||
// with a comma-decimal locale (e.g., de_DE) would incorrectly interpret the dot
|
||||
// as a thousands separator, causing 5.5 to be saved as 5.
|
||||
$batch_save_data = [
|
||||
'default_tax_1_rate' => parse_tax(filter_var($default_tax_1_rate, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION)),
|
||||
'default_tax_1_rate' => (float) filter_var($default_tax_1_rate, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION),
|
||||
'default_tax_1_name' => $this->request->getPost('default_tax_1_name'),
|
||||
'default_tax_2_rate' => parse_tax(filter_var($default_tax_2_rate, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION)),
|
||||
'default_tax_2_rate' => (float) filter_var($default_tax_2_rate, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION),
|
||||
'default_tax_2_name' => $this->request->getPost('default_tax_2_name'),
|
||||
'tax_included' => $this->request->getPost('tax_included') != null,
|
||||
'use_destination_based_tax' => $this->request->getPost('use_destination_based_tax') != null,
|
||||
|
||||
@@ -161,7 +161,7 @@ class Sales extends Secure_Controller
|
||||
'only_bank_transfer'=> false,
|
||||
'only_wallet' => false,
|
||||
'only_invoices' => $this->config['invoice_enable'] && $this->request->getGet('only_invoices', FILTER_SANITIZE_NUMBER_INT),
|
||||
'is_valid_receipt' => $this->sale->isValidReceipt($search)
|
||||
'is_valid_receipt' => $this->sale->is_valid_receipt($search)
|
||||
];
|
||||
|
||||
// Check if any filter is set in the multiselect dropdown
|
||||
@@ -198,7 +198,7 @@ class Sales extends Secure_Controller
|
||||
? $this->request->getGet('term')
|
||||
: null;
|
||||
|
||||
if ($this->sale_lib->get_mode() == 'return' && $this->sale->isValidReceipt($receipt)) {
|
||||
if ($this->sale_lib->get_mode() == 'return' && $this->sale->is_valid_receipt($receipt)) {
|
||||
// If a valid receipt or invoice was found the search term will be replaced with a receipt number (POS #)
|
||||
$suggestions[] = $receipt;
|
||||
}
|
||||
@@ -525,7 +525,7 @@ class Sales extends Secure_Controller
|
||||
$quantity = ($mode == 'return') ? -$quantity : $quantity;
|
||||
$item_location = $this->sale_lib->get_sale_location();
|
||||
|
||||
if ($mode == 'return' && $this->sale->isValidReceipt($item_id_or_number_or_item_kit_or_receipt)) {
|
||||
if ($mode == 'return' && $this->sale->is_valid_receipt($item_id_or_number_or_item_kit_or_receipt)) {
|
||||
$this->sale_lib->return_entire_sale($item_id_or_number_or_item_kit_or_receipt);
|
||||
} elseif ($this->item_kit->is_valid_item_kit($item_id_or_number_or_item_kit_or_receipt)) {
|
||||
// Add kit item to order if one is assigned
|
||||
|
||||
@@ -25,7 +25,7 @@ class MY_Migration extends MigrationRunner
|
||||
public function get_latest_migration(): int
|
||||
{
|
||||
$migrations = $this->findMigrations();
|
||||
return (int) basename(end($migrations)->version);
|
||||
return basename(end($migrations)->version);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,7 +41,7 @@ class MY_Migration extends MigrationRunner
|
||||
$builder = $db->table('migrations');
|
||||
$builder->select('version')->orderBy('version', 'DESC')->limit(1);
|
||||
$result = $builder->get()->getRow();
|
||||
return $result ? (int) $result->version : 0;
|
||||
return $result ? $result->version : 0;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Database not available yet (e.g. fresh install before schema).
|
||||
|
||||
@@ -327,7 +327,7 @@ class Sale extends Model
|
||||
{
|
||||
$suggestions = [];
|
||||
|
||||
if (!$this->isValidReceipt($search)) {
|
||||
if (!$this->is_valid_receipt($search)) {
|
||||
$builder = $this->db->table('sales');
|
||||
$builder->distinct()->select('first_name, last_name');
|
||||
$builder->join('people', 'people.person_id = sales.customer_id');
|
||||
@@ -408,21 +408,21 @@ class Sale extends Model
|
||||
/**
|
||||
* Checks if valid receipt
|
||||
*/
|
||||
public function isValidReceipt(string|null &$receiptSaleId): bool // TODO: like the others, maybe this should be an array rather than a delimited string... either that or the parameter name needs to be changed. $receipt_sale_id implies that it's an int.
|
||||
public function is_valid_receipt(string|null &$receipt_sale_id): bool // TODO: like the others, maybe this should be an array rather than a delimited string... either that or the parameter name needs to be changed. $receipt_sale_id implies that it's an int.
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
|
||||
if (!empty($receiptSaleId)) {
|
||||
if (!empty($receipt_sale_id)) {
|
||||
// POS #
|
||||
$pieces = explode(' ', trim($receiptSaleId));
|
||||
$pieces = explode(' ', $receipt_sale_id);
|
||||
|
||||
if (count($pieces) == 2 && strtoupper($pieces[0]) === 'POS' && ctype_digit($pieces[1])) {
|
||||
return $this->exists((int)$pieces[1]);
|
||||
if (count($pieces) == 2 && preg_match('/(POS)/i', $pieces[0])) {
|
||||
return $this->exists($pieces[1]);
|
||||
} elseif ($config['invoice_enable']) {
|
||||
$saleInfo = $this->get_sale_by_invoice_number($receiptSaleId);
|
||||
$sale_info = $this->get_sale_by_invoice_number($receipt_sale_id);
|
||||
|
||||
if ($saleInfo->getNumRows() > 0) {
|
||||
$receiptSaleId = 'POS ' . $saleInfo->getRow()->sale_id;
|
||||
if ($sale_info->getNumRows() > 0) {
|
||||
$receipt_sale_id = 'POS ' . $sale_info->getRow()->sale_id;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
'name' => 'default_tax_1_rate',
|
||||
'id' => 'default_tax_1_rate',
|
||||
'class' => 'form-control input-sm',
|
||||
'value' => to_tax_decimals($config['default_tax_1_rate'])
|
||||
'value' => $config['default_tax_1_rate'] !== '' ? (float) $config['default_tax_1_rate'] : ''
|
||||
]) ?>
|
||||
<span class="input-group-addon input-sm">%</span>
|
||||
</div>
|
||||
@@ -83,7 +83,7 @@
|
||||
'name' => 'default_tax_2_rate',
|
||||
'id' => 'default_tax_2_rate',
|
||||
'class' => 'form-control input-sm',
|
||||
'value' => to_tax_decimals($config['default_tax_2_rate'])
|
||||
'value' => $config['default_tax_2_rate'] !== '' ? (float) $config['default_tax_2_rate'] : ''
|
||||
]) ?>
|
||||
<span class="input-group-addon input-sm">%</span>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ use CodeIgniter\Test\CIUnitTestCase;
|
||||
use CodeIgniter\Test\DatabaseTestTrait;
|
||||
use CodeIgniter\Test\FeatureTestTrait;
|
||||
use CodeIgniter\Config\Services;
|
||||
use App\Models\Appconfig;
|
||||
|
||||
class ConfigTest extends CIUnitTestCase
|
||||
{
|
||||
@@ -218,4 +219,108 @@ class ConfigTest extends CIUnitTestCase
|
||||
$result = json_decode($response->getJSON(), true);
|
||||
$this->assertFalse($result['success']);
|
||||
}
|
||||
|
||||
// ========== Tax Rate Locale Tests ==========
|
||||
// These tests verify that tax rate inputs (type="number") work correctly
|
||||
// regardless of locale settings. Browsers always submit type="number" inputs
|
||||
// as dot-decimal values, so the server must handle them correctly without
|
||||
// using locale-aware parse_tax() which would misinterpret the dot.
|
||||
|
||||
public function testTaxRate_SavesDotDecimalValueCorrectly(): void
|
||||
{
|
||||
$this->resetSession();
|
||||
|
||||
// type="number" inputs always submit dot-decimal "5.5", not comma-decimal "5,5"
|
||||
$response = $this->post('/config/saveTax', [
|
||||
'default_tax_1_rate' => '5.5',
|
||||
'default_tax_1_name' => 'Tax 1',
|
||||
'tax_included' => '0',
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$result = json_decode($response->getJSON(), true);
|
||||
$this->assertTrue($result['success']);
|
||||
|
||||
// Verify the value was saved correctly as 5.5, not truncated to 5
|
||||
$config = model(Appconfig::class);
|
||||
$savedRate = $config->get_value('default_tax_1_rate');
|
||||
$this->assertEquals(5.5, (float) $savedRate, 'Tax rate should be saved as 5.5, not truncated to 5');
|
||||
}
|
||||
|
||||
public function testTaxRate_SavesIntegerValueCorrectly(): void
|
||||
{
|
||||
$this->resetSession();
|
||||
|
||||
$response = $this->post('/config/saveTax', [
|
||||
'default_tax_1_rate' => '18',
|
||||
'default_tax_1_name' => 'VAT',
|
||||
'tax_included' => '0',
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$result = json_decode($response->getJSON(), true);
|
||||
$this->assertTrue($result['success']);
|
||||
|
||||
$config = model(Appconfig::class);
|
||||
$savedRate = $config->get_value('default_tax_1_rate');
|
||||
$this->assertEquals(18.0, (float) $savedRate, 'Tax rate should be saved as 18');
|
||||
}
|
||||
|
||||
public function testTaxRate_SavesHighPrecisionDecimal(): void
|
||||
{
|
||||
$this->resetSession();
|
||||
|
||||
$response = $this->post('/config/saveTax', [
|
||||
'default_tax_1_rate' => '8.25',
|
||||
'default_tax_1_name' => 'Sales Tax',
|
||||
'tax_included' => '0',
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$result = json_decode($response->getJSON(), true);
|
||||
$this->assertTrue($result['success']);
|
||||
|
||||
$config = model(Appconfig::class);
|
||||
$savedRate = $config->get_value('default_tax_1_rate');
|
||||
$this->assertEquals(8.25, (float) $savedRate, 'Tax rate should preserve decimal precision');
|
||||
}
|
||||
|
||||
public function testTaxRate_BothTaxRatesSavedCorrectly(): void
|
||||
{
|
||||
$this->resetSession();
|
||||
|
||||
$response = $this->post('/config/saveTax', [
|
||||
'default_tax_1_rate' => '10.5',
|
||||
'default_tax_1_name' => 'State Tax',
|
||||
'default_tax_2_rate' => '5.25',
|
||||
'default_tax_2_name' => 'Local Tax',
|
||||
'tax_included' => '0',
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$result = json_decode($response->getJSON(), true);
|
||||
$this->assertTrue($result['success']);
|
||||
|
||||
$config = model(Appconfig::class);
|
||||
$savedRate1 = $config->get_value('default_tax_1_rate');
|
||||
$savedRate2 = $config->get_value('default_tax_2_rate');
|
||||
|
||||
$this->assertEquals(10.5, (float) $savedRate1, 'Tax 1 rate should be 10.5');
|
||||
$this->assertEquals(5.25, (float) $savedRate2, 'Tax 2 rate should be 5.25');
|
||||
}
|
||||
|
||||
public function testTaxRate_HandlesEmptyString(): void
|
||||
{
|
||||
$this->resetSession();
|
||||
|
||||
$response = $this->post('/config/saveTax', [
|
||||
'default_tax_1_rate' => '',
|
||||
'default_tax_1_name' => 'Tax 1',
|
||||
'tax_included' => '0',
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$result = json_decode($response->getJSON(), true);
|
||||
$this->assertTrue($result['success']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user