It's my first time setting up an e-commerce website, and first time working with sql and php.
I'm working with 1.5.5.1
I went through the code, and I think currency conversion is already rounded per product price to the corresponding decimal place for the currency being used.
The problem is reduced if you use prices with 2 decimal places (such as 19.90, not 19.9125), or whatever paypal limits to for each currency.
There is a problem however, with tax.
If you want a 19% VAT price of 189.00, your product price would be 158.8235. But you can't enter this into your product price because you'll get the rounding error. If you entered 158.82, your VAT price would be 188.9958, and you'd get the rounding error again whenever taxes are being calculated for multiple quantities of the same product.
With the example from TomTom:
Product price 16.3865 (+ 19% VAT = 19.4999 )
Product price in frontend 19.50
2 x 16.3865 = 32.773 (x 1.19 = 38.9998 )
Product price in frontend 39.00
However in PayPal the cart says 39.01 (! wtf !)
Paypal is taking 16.39 x 2 = 32.78
Opencart tax is 16.3865 x 0.19 x 2 = 6.2269 or 6.23
32.78+6.23 = 39.01
The error is the mismatch between the rounding of the product price.
So instead, what TomTom should do is set his product price to 16.39.
His VAT is 3.1141, and VAT incl. price is 19.5041. (frontend price of 19.50)
So if someone orders 2 products, they will get in opencart:
subtotal 16.39 * 2 = 32.78
tax 3.1141 *2 = 6.2282
opencart total = 39.0082, rounded to 39.01.
Paypal should technically charge the same, 39.01.
But TomTom wants to have 39.00.
So what we need to do is to round the tax for each quantity of the product, before it is totaled.
We want tax 3.1141 changed to 3.11.
Then 3.11 x 2 = 6.22
6.22 +32.78 = 39.00
Tax is calculated for each single product in the following function:
In
system/library/tax.php, getRates()
For the following lines:
Code: Select all
if ($tax_rate['type'] == 'F') {
$amount += $tax_rate['rate'];
} elseif ($tax_rate['type'] == 'P') {
$amount += ($value / 100 * $tax_rate['rate']);
}
To round the tax to 2 decimal places, this could be changed to:
Code: Select all
if ($tax_rate['type'] == 'F') {
$amount += round($tax_rate['rate'], 2, PHP_ROUND_HALF_UP);
} elseif ($tax_rate['type'] == 'P') {
$amount += round(($value / 100 * $tax_rate['rate']), 2, PHP_ROUND_HALF_UP);
}
This should work if you are using USD and EURO where there are 2 decimal places.
However, if you switch to a currency with more than 2 decimal places, it will be a problem because the function structures do not pass down the currency desired.
Whereas, when currency is converted and calculated in
system/library/currency.php, format(), the value is rounded to the decimal place that is pulled out of the database in reference to the currency that is passed down:
Code: Select all
$decimal_place = $this->currencies[$currency]['decimal_place'];
and
Code: Select all
$string .= number_format(round($value, (int)$decimal_place), (int)$decimal_place, $decimal_point, $thousand_point);
In order to get the currency code passed down to pull out the decimal place required during tax calculations, you'd have to change every call for tax->calculate, getTax, and getRates to include the currency required, and include the
$currency variable in the function structures.
Then you have to pull out the currency info from the database
Code: Select all
$query = $this->db->query("SELECT * FROM " . DB_PREFIX . "currency");
foreach ($query->rows as $result) {
$this->currencies[$result['code']] = array(
'currency_id' => $result['currency_id'],
'title' => $result['title'],
'symbol_left' => $result['symbol_left'],
'symbol_right' => $result['symbol_right'],
'decimal_place' => $result['decimal_place'],
'value' => $result['value']
);
}
Then grab the required decimal place:
Code: Select all
$decimal_place = $this->currencies[$currency]['decimal_place'];
And then finally we can round to the required decimal place value:
Code: Select all
if ($tax_rate['type'] == 'F') {
$amount += round($tax_rate['rate'], $decimal_place, PHP_ROUND_HALF_UP);
} elseif ($tax_rate['type'] == 'P') {
$amount += round(($value / 100 * $tax_rate['rate']), $decimal_place, PHP_ROUND_HALF_UP);
}
I myself encountered the problem where for example I have a product which is:
price: 10.38
Incl. 19% vat price: 12.3522 (frontend 12.35)
quantity: 10
total price displayed: 123.52 (expected 123.50)
This is because the tax is 1.9722
1.9722 * 10 = 19.722
With the new rounding, we would get:
1.97 * 10 = 19.70
and the 2 cents are knocked off.
However, we have the problem that we can't get 19.95 incl. 19% VAT.
Because, 16.76 * 1.19 = 19.9444 (rounded to 19.94)
16.76 * 0.19 = 3.1844
and 16.77 * 1.19 = 19.9563 (rounded to 19.96)
16.77 * 0.19 = 3.1863
We shouldn't use more than 2 decimal places in the product price because this will introduce the same problem again with the mismatch in subtotal between opencart and PayPal.
Or if you really want that 19.95, you could try PHP_ROUND_HALF_ODD, but I don't know if this is acceptable in standard accounting, and this may not always give you a 0.95. You my end up with 0.94 or 0.96 with a different number, as ODD is always applied.
You could possibly insert a logic that calculates the final price and if either ODD or EVEN gets you the 0.95, then to apply the correct one.
But this solution only works for countries where the price must include VAT, like in Germany. Because in a country like the USA where prices are not displayed with tax, the tax should be calculated based on the final price, and not rounded values for each quantity of product.
So for example, if I'm in the US, and I want to buy something for 9.95 excl. tax, with a 9.5% tax.
If I applied the modification above, I would get:
price: 9.95
tax: 0.9453 (rounded to 0.95)
quantity: 10
total: (9.95 + 0.95) * 10 = 109.00
However, standard accounting practice requires calculating the tax on the total price:
10 * 9.95 * 1.095 = 108.9525 (rounded to 108.95)
So in effect, Opencart should work well out of the box for countries where tax is not displayed in the price, like the USA, because the tax is based on the subtotal, as long as you stay with 2 decimal place prices to match with PayPal. If you are in a country like Germany where you need to display prices with VAT, you need to make the modification above to round the tax for each item, and not the total, because the tax is based on the individual item.
My post is a bit long, because I'm sort of figuring it out as I type.
Ah, but then we have another problem, as in Germany it is allowed to display prices without VAT if you are a seller B2B, but if the business customer is located in Germany then VAT has to be charge. In this case, the VAT should then be calculated based on the subtotal, and not on the individual item.
In this case, opencart should be redesigned for allowing prices to be displayed with or without tax, based on the customer group, so with tax to consumers, and without tax for business customers, and then applying or disabling the tax rounding modifications above. However, as I look in the admin, the disabling or enabling of showing tax in prices is global and can't be set by customer group.
And so that's my insight into the calculation problems with opencart.
I haven't tried this modification.
Someone try it and let me know!
But I take no responsibility and make no warranty to its effects.