← Back to team overview

openerp-expert-accounting team mailing list archive

Re: Floating point precisions, balanced entries and VAT included reflex

 

>
>
> Hello,

I forward here a mail by Cedric Krier (Tryton lead) where he explains why
they went Decimal in Tryton and what they did to accommodate to VAT included
price system (they choose not implementing it at the core):


Hi,

Here is some reflections we had 2 years ago when starting Tryton.


* price_accuracy
================

I know well the price_accurary parameter because I wrote the implementation
and I did not agree with this choice. And it seems that his usage has been
changed moving away from his original objectif (define internal precision
for
computation of amount like unit price). Here is some issues:

   - Global for every databases and every context.
   - Could not be changed without database migration.
   - Used in some cases for rounding amount instead of currency value.

We choice to go in a different way. First, we allow to have the digits
attributes on Float and Decimal fields depending on other field values. A
common practice is to have:

   total_amount = fields.Function('get_total_amount', type='numeric',
           digits=(16, Eval('currency_digits', 2)), string='Total',
           fnct_search='search_total_amount')

where currency_digits is a field that contains the number of decimals that
the
currency defined.

Second, the digits value of a field is not used in the definition of the
database field. This is to allow to change the value with custom module
without migration. By example:

   unit_price = fields.Numeric('Unit Price', digits=(16, 4),
           states={
               'invisible': Not(Equal(Eval('type'), 'line')),
               'required': Equal(Eval('type'), 'line'),
           })

This is the definition of the unit_price of an invoice line. We used by
default a precision of 4 numbers after decimal separator. But the database
definition is:

   unit_price   | numeric                     | not null default 0

So a custom module could change the value of decimal number without any
problem. He can even make it depends of a field on product per example.

Of course the developer is responsible to correctly round the values before
writing it. (But I will work on a validate function for Tryton next version
that will raise exception if developer forget it).


* Decimal
=========

Here is some example that shows why we use Decimal when we work with money
(or
anything that requires right computation):

   >>> 0.1 + 0.1 + 0.1 == 0.3
   False

0.1 can not be actualy represented with a float.

   >>> round(0.1, 1) + round(0.1, 1) + round(0.1, 1) == 0.3
   False

Rounding each value doesn't solve the issue.

   >>> round(0.1 + 0.1 + 0.1, 1) == 0.3
   True

You must round the finally amount to have correct result.

   >>> Decimal('0.1') + Decimal('0.1') + Decimal('0.1') == Decimal('0.3')
   True

Using decimal work out of the box.

Of course if you require specific precision you need to round the result but
for comparaison it is much easier and readable.

   >>> (10000000.2 - 10000000) * 10000000 == 2000000
   False

You can think you need to round the multiplication result.

   >>> round((10000000.2 - 10000000) * 10000000, 2) == 2000000
   False

It doesn't work. The right solution is to round every operations.

   >>> round(round((10000000.2 - 10000000), 2) * 10000000, 2) == 2000000
   True

But with Decimal.

   >>> (Decimal('10000000.2') - Decimal('10000000')) * Decimal('10000000')
== Decimal('2000000')
   True

So you can see that using float, you must round at every operations and even
with that you can have precision lost because of rounding too much internal
result. The main issue with float is that in some cases the error of
floating
precision can become significant (and it doesn't require to have big
values).

We also think that Tryton is a framework so we must not limit the scope to
the
current modules but we must try to provide the best tools for developing any
business solution. So even if using float can be fixed to work for the
common
case of sale, purchase and invoice, we must think about the possibility to
have a interest computation for a loan module etc.

For all of this reasons, we choose to work with Decimal even if the
computation
is slower, the code is more readable, more accurate and more generic.
And by the way, PostgreSQL already make decimal computation.


* Tax included price
====================

The issue with tax included price is that computation of taxes amount is not
commutative (due to rounding operation). Here is an example with a belgian
case:

   Having the tax included price equals to 100
   Having the tax equals to 21%

   So the computation of the tax exclude is:

       >>> round(100 / 1.21, 2)
       82.640000000000001

   Here comes back the floating point precision issue. So let's go with
   Decimal.

       >>> (Decimal('100') / Decimal('1.21')).quantize(Decimal('0.01'))
       Decimal('82.64')

   If I try to compute back the amount with tax included:

       >>> (Decimal('82.64') * Decimal('1.21')).quantize(Decimal('0.01'))
       Decimal('99.99')

   So you can try by rounding up:

       >>> (Decimal('100') / Decimal('1.21')).quantize(Decimal('0.01'),
rounding=decimal.ROUND_UP)
       Decimal('82.65')

   And tax computation:

       >>> (Decimal('82.65') * Decimal('1.21')).quantize(Decimal('0.01'),
rounding=decimal.ROUND_UP)
       Decimal('100.01')

   Of course you can find the right value if you mix rounding method but
this
   is not a generic way (and could not be legal).

So you must accept that working with tax included you must have to customize
the tax amount and the untaxed amount by using an algorithm to spread the
rounding lost precision (this algorithm is generaly custom according to the
company and country rules).

In Tryton, we choice to not handle this at the invoice level because it
makes
the code too complex and error prone. We think that the accounting system
must
only work with untaxed amounts.

So to resolv the POS issue, we made specific model to store sales with tax
included amount and it generates account moves according to the choosen way
to
spread the rounding issue. (it could also generate invoice with custom tax
amount) We already made a custom module with this behavior (not published
because too specific).

PS: fell free to forward it if you think it is useful
--
Cédric Krier

B2CK SPRL
Rue de Rotterdam, 4
4000 Liège
Belgium
Tel: +32 472 54 46 59
Email/Jabber: cedric.krier@xxxxxxxx
Website: http://www.b2ck.com/

References