Compare commits

...

2 Commits

Author SHA1 Message Date
Ollama
6633bb36a8 fix: tax rate input locale handling - save path
The display fix (using (float) instead of to_tax_decimals()) was
correct but incomplete. The save path in Config.php also needed
fixing because parse_tax() misinterprets dot-decimal values from
type="number" inputs when locale uses comma as decimal separator.

Root cause: Browsers submit type="number" inputs as dot-decimal
(e.g., "5.5") regardless of locale. With comma-decimal locales
like de_DE, parse_tax() treats the dot as thousands separator,
causing 5.5 to be saved as 5.

Fix: Replace parse_tax() with direct (float) cast for these
inputs since type="number" already guarantees dot-decimal format.

Includes tests for tax rate handling with various decimal values.

Fixes #4553
2026-05-22 19:06:41 +02:00
Ollama
30d5ac4496 fix: tax rate inputs blank with comma-decimal locales
The to_tax_decimals() function returns locale-formatted values
(e.g. "18,00" for comma-decimal locales like fr_FR, de_DE).
Browsers reject comma-decimal values in <input type="number">
and render the field blank.

Use raw float value instead - PHP serializes floats with period
decimal regardless of locale. The parse_tax() on the save side
already handles locale-aware parsing, so round-tripping works
correctly.

Fixes #4553
Regression from commit 42ba39d29
2026-05-21 21:36:14 +02:00
3 changed files with 113 additions and 4 deletions

View File

@@ -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,

View File

@@ -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>

View File

@@ -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']);
}
}