From dcb5ff6eacc63cebdc03f8615b18a002cb6ad23d Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 14 Nov 2011 17:36:17 +0100 Subject: [PATCH 01/58] [IMP] res.currency,base: factor out float rounding methods in tools to allow use without currency Pending: addition of tests bzr revid: odo@openerp.com-20111114163617-kw7u8rxcie7m2hj5 --- openerp/addons/base/res/res_currency.py | 41 ++++++++++--- openerp/tools/misc.py | 79 ++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 9 deletions(-) diff --git a/openerp/addons/base/res/res_currency.py b/openerp/addons/base/res/res_currency.py index 32aa387e828..c0a2bebc104 100644 --- a/openerp/addons/base/res/res_currency.py +++ b/openerp/addons/base/res/res_currency.py @@ -24,7 +24,7 @@ import netsvc from osv import fields, osv import tools -from tools.misc import currency +from tools.misc import currency, float_round, float_is_zero, float_compare from tools.translate import _ CURRENCY_DISPLAY_PATTERN = re.compile(r'(\w+)\s*(?:\((.*)\))?') @@ -127,15 +127,40 @@ class res_currency(osv.osv): return [(x['id'], tools.ustr(x['name']) + (x['symbol'] and (' (' + tools.ustr(x['symbol']) + ')') or '')) for x in reads] def round(self, cr, uid, currency, amount): - if currency.rounding == 0: - return 0.0 - else: - # /!\ First member below must be rounded to full unit! - # Do not pass a rounding digits value to round() - return round(amount / currency.rounding) * currency.rounding + """Return ``amount`` rounded according to ``currency``'s + rounding rules, also minimizing IEEE-754 floating point + representation errors. + + :param browse_record currency: currency for which we are rounding + :param float amount: the amount to round + :return: rounded float + """ + return float_round(amount, precision_rounding=currency.rounding) + + def compare_amounts(self, cr, uid, currency, amount1, amount2): + """Compare ``amount1`` and ``amount2`` according to ``currency``'s + rounding rules, and return (resp.) -1, 0 or 1, if ``amount1`` + is (resp.) lower than, equal to, or greater than ``amount2``. + + For example 1.432 and 1.431 are equal if currency is rounded to + 2 digits, so this method would return 0 + + :param browse_record currency: currency for which we are rounding + :param float amount1: first amount to compare + :param float amount2: second amount to compare + :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than, + equal to, or greater than ``amount2``, according to + ``currency``'s rounding. + """ + return float_compare(amount1, amount2, precision_rounding=currency.rounding) def is_zero(self, cr, uid, currency, amount): - return abs(self.round(cr, uid, currency, amount)) < currency.rounding + """Returns true if ``amount`` is small enough to be treated as + zero according to ``currency``'s rounding rules. + :param browse_record currency: currency for which we are rounding + :param float amount: amount to compare with currency's zero + """ + return float_is_zero(amount, precision_rounding=currency.rounding) def _get_conversion_rate(self, cr, uid, from_currency, to_currency, context=None): if context is None: diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index dbccbe5b217..ae28567e19b 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -1200,4 +1200,81 @@ class UnquoteEvalContext(defaultdict): def __missing__(self, key): return unquote(key) -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: +def _float_check_precision(precision_digits=None, precision_rounding=None): + assert (precision_digits is not None or precision_rounding is not None) and \ + not (precision_digits and precision_rounding),\ + "exactly one of precision_digits and precision_rounding must be specified" + if precision_digits is not None: + return 10 ** -precision_digits + return precision_rounding + +def float_round(amount, precision_digits=None, precision_rounding=None): + """Return ``amount`` rounded to ``precision_digits`` + decimal digits, minimizing IEEE-754 floating point representation + errors. + Precision must be given by ``precision_digits`` or ``precision_rounding``, + not both! + Example on Python 2.7.2:: + + >>> round_float(2.675) + 2.68 + >>> round(2.675,2) + 2.67 + + :param float amount: the amount to round + :param int precision_digits: number of decimal digits to round to. + :param float precision_rounding: decimal number representing the minimum + non-zero value at the desired precision (for example, 0.01 for a + 2-digit precision). + :return: rounded float + """ + rounding_factor = _float_check_precision(precision_digits=precision_digits, + precision_rounding=precision_rounding) + if rounding_factor == 0: return 0.0 + # /!\ First member below must be rounded to full unit! + # Do not pass rounding digits to round()! + return round(amount / rounding_factor) * rounding_factor + +def float_is_zero(amount, precision_digits=None, precision_rounding=None): + """Returns true if ``amount`` is small enough to be treated as + zero at the given precision. + Precision must be given by ``precision_digits`` or ``precision_rounding``, + not both! + + :param int precision_digits: number of decimal digits to round to. + :param float precision_rounding: decimal number representing the minimum + non-zero value at the desired precision (for example, 0.01 for a + 2-digit precision). + :param float amount: amount to compare with currency's zero + :return: True if ``amount`` is considered 0 + """ + rounding_factor = _float_check_precision(precision_digits=precision_digits, + precision_rounding=precision_rounding) + return abs(float_round(amount, precision_rounding=rounding_factor)) < rounding_factor + +def float_compare(amount1, amount2, precision_digits=None, precision_rounding=None): + """Compare ``amount1`` and ``amount2`` according + to the given precision. + Precision must be given by ``precision_digits`` or ``precision_rounding``, + not both! + + For example 1.432 and 1.431 are equal at 2 digits precision, + so this method would return 0 + + :param int precision_digits: number of decimal digits to round to. + :param float precision_rounding: decimal number representing the minimum + non-zero value at the desired precision (for example, 0.01 for a + 2-digit precision). + :param float amount1: first amount to compare + :param float amount2: second amount to compare + :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than, + equal to, or greater than ``amount2``, at the given precision. + """ + rounding_factor = _float_check_precision(precision_digits=precision_digits, + precision_rounding=precision_rounding) + delta = amount1 - amount2 + if float_is_zero(delta, precision_rounding=rounding_factor): return 0 + delta = float_round(delta, precision_rounding=rounding_factor) + return -1 if delta < 0 else 1 + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file From ffe640cecea84380fd11e9a05bb6bc4882fef852 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 14 Nov 2011 19:23:10 +0100 Subject: [PATCH 02/58] [IMP] tools.float_*: added tests and docstrings bzr revid: odo@openerp.com-20111114182310-kkzh78uej0uuwhaz --- openerp/addons/base/res/res_currency.py | 23 ++++++--- openerp/addons/base/test/base_test.yml | 68 +++++++++++++++++++++++++ openerp/tools/misc.py | 31 ++++++++--- 3 files changed, 107 insertions(+), 15 deletions(-) diff --git a/openerp/addons/base/res/res_currency.py b/openerp/addons/base/res/res_currency.py index c0a2bebc104..c67c7d65260 100644 --- a/openerp/addons/base/res/res_currency.py +++ b/openerp/addons/base/res/res_currency.py @@ -128,8 +128,7 @@ class res_currency(osv.osv): def round(self, cr, uid, currency, amount): """Return ``amount`` rounded according to ``currency``'s - rounding rules, also minimizing IEEE-754 floating point - representation errors. + rounding rules. :param browse_record currency: currency for which we are rounding :param float amount: the amount to round @@ -138,12 +137,16 @@ class res_currency(osv.osv): return float_round(amount, precision_rounding=currency.rounding) def compare_amounts(self, cr, uid, currency, amount1, amount2): - """Compare ``amount1`` and ``amount2`` according to ``currency``'s - rounding rules, and return (resp.) -1, 0 or 1, if ``amount1`` - is (resp.) lower than, equal to, or greater than ``amount2``. + """Compare ``amount1`` and ``amount2`` after rounding them according to the + given currency's precision.. + An amount is considered lower/greater than another amount if their rounded + value is different. This is not the same as having a non-zero difference! - For example 1.432 and 1.431 are equal if currency is rounded to - 2 digits, so this method would return 0 + For example 1.432 and 1.431 are equal at 2 digits precision, + so this method would return 0. + However 0.006 and 0.002 are considered different (returns 1) because + they respectively round to 0.01 and 0.0, even though + 0.006-0.002 = 0.004 which would be considered zero at 2 digits precision. :param browse_record currency: currency for which we are rounding :param float amount1: first amount to compare @@ -157,6 +160,12 @@ class res_currency(osv.osv): def is_zero(self, cr, uid, currency, amount): """Returns true if ``amount`` is small enough to be treated as zero according to ``currency``'s rounding rules. + + Warning: ``is_zero(amount1-amount2)`` is not always equivalent to + ``compare_amounts(amount1,amount2) == 0``, as the former will round after + computing the difference, while the latter will round before, giving + different results for e.g. 0.006 and 0.002 at 2 digits precision. + :param browse_record currency: currency for which we are rounding :param float amount: amount to compare with currency's zero """ diff --git a/openerp/addons/base/test/base_test.yml b/openerp/addons/base/test/base_test.yml index 6ea578a8659..9b686ccb0d3 100644 --- a/openerp/addons/base/test/base_test.yml +++ b/openerp/addons/base/test/base_test.yml @@ -144,3 +144,71 @@ !python {model: res.partner.category}: | self.pool._init = True +- + "Float precision tests: verify that float rounding methods are working correctly" +- + !python {model: res.currency}: | + currency = self.browse(cr, uid, ref('base.EUR')) + def try_round(self, cr, currency, amount, expected): + result = str(self.round(cr, 1, currency, amount)) + assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected) + try_round(self, cr, currency, 2.674,'2.67') + try_round(self, cr, currency, 2.675,'2.68') # in Python 2.7.2, round(2.675,2) gives 2.67 + try_round(self, cr, currency, 0.001,'0.0') + try_round(self, cr, currency, 0.0049,'0.0') # 0.0049 is closer to 0 than to 0.01, so should round down + try_round(self, cr, currency, 0.005,'0.01') # the rule is to round half up + + def try_zero(self, cr, currency, amount, expected): + assert self.is_zero(cr, 1, currency, amount) == expected, "Rounding error: %s should be zero!" % amount + try_zero(self, cr, currency, 0.01, False) + try_zero(self, cr, currency, 0.001, True) + try_zero(self, cr, currency, 0.0046, True) + try_zero(self, cr, currency, 2.68-2.675, False) # 2.68 - 2.675 = 0.005 -> rounds to 0.01 + try_zero(self, cr, currency, 2.68-2.676, True) # 2.68 - 2.675 = 0.004 -> rounds to 0.0 + + def try_compare(self, cr, currency, amount1, amount2, expected): + assert self.compare_amounts(cr, 1, currency, amount1, amount2) == expected, \ + "Rounding error, compare_amounts(%s,%s) should be %s" % (amount1, amount2, expected) + try_compare(self, cr, currency, 0.001, 0.001, 0) + try_compare(self, cr, currency, 0.001, 0.002, 0) + try_compare(self, cr, currency, 2.675, 2.68, 0) + try_compare(self, cr, currency, 2.676, 2.68, 0) + try_compare(self, cr, currency, 2.674, 2.68, -1) + try_compare(self, cr, currency, 3, 2.68, 1) + try_compare(self, cr, currency, 0.01, 0, 1) + + from tools import float_compare, float_is_zero, float_round + def try_round_digits(float_round, amount, expected): + result = str(float_round(amount, precision_digits=3)) + assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected) + try_round_digits(float_round, 2.6745, '2.675') + try_round_digits(float_round, 2.6744, '2.674') + try_round_digits(float_round, 0.0004, '0.0') + + def try_zero_digits(float_is_zero, amount, expected): + assert float_is_zero(amount, precision_digits=3) == expected, "Rounding error: %s should be zero!" % amount + try_zero_digits(float_is_zero, 0.0002, True) + try_zero_digits(float_is_zero, 0.00034, True) + try_zero_digits(float_is_zero, 0.0005, False) + try_zero_digits(float_is_zero, 0.0008, False) + + def try_compare_digits(float_compare, amount1, amount2, expected): + assert float_compare(amount1, amount2, precision_digits=3) == expected, \ + "Rounding error, compare_amounts(%s,%s) should be %s" % (amount1, amount2, expected) + try_compare_digits(float_compare, 0.0003, 0.0004, 0) + try_compare_digits(float_compare, 0.0002, 0.0005, -1) + try_compare_digits(float_compare, 0.0009, 0.0004, 1) + + # specifying 2 precisions is illegal: + try: + float_is_zero(0.01, precision_digits=3, precision_rounding=0.01) + except AssertionError: + pass + try: + float_compare(0.01, 0.02, precision_digits=3, precision_rounding=0.01) + except AssertionError: + pass + try: + float_round(0.01, precision_digits=3, precision_rounding=0.01) + except AssertionError: + pass diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index ae28567e19b..496767f7c41 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -1210,11 +1210,13 @@ def _float_check_precision(precision_digits=None, precision_rounding=None): def float_round(amount, precision_digits=None, precision_rounding=None): """Return ``amount`` rounded to ``precision_digits`` - decimal digits, minimizing IEEE-754 floating point representation + decimal digits, minimizing IEEE-854 floating point representation errors. Precision must be given by ``precision_digits`` or ``precision_rounding``, not both! - Example on Python 2.7.2:: + + To illustrate how this is different from the default round() builtin, + here is an example (depends on Python version, here is for v2.7.2 x64):: >>> round_float(2.675) 2.68 @@ -1241,6 +1243,11 @@ def float_is_zero(amount, precision_digits=None, precision_rounding=None): Precision must be given by ``precision_digits`` or ``precision_rounding``, not both! + Warning: ``float_is_zero(amount1-amount2)`` is not always equivalent to + ``float_compare(amount1,amount2) == 0``, as the former will round after + computing the difference, while the latter will round before, giving + different results for e.g. 0.006 and 0.002 at 2 digits precision. + :param int precision_digits: number of decimal digits to round to. :param float precision_rounding: decimal number representing the minimum non-zero value at the desired precision (for example, 0.01 for a @@ -1253,13 +1260,20 @@ def float_is_zero(amount, precision_digits=None, precision_rounding=None): return abs(float_round(amount, precision_rounding=rounding_factor)) < rounding_factor def float_compare(amount1, amount2, precision_digits=None, precision_rounding=None): - """Compare ``amount1`` and ``amount2`` according - to the given precision. - Precision must be given by ``precision_digits`` or ``precision_rounding``, - not both! + """Compare ``amount1`` and ``amount2`` after rounding them according to the + given precision. An amount is considered lower/greater than another amount + if their rounded value is different. This is not the same as having a + non-zero difference! For example 1.432 and 1.431 are equal at 2 digits precision, so this method would return 0 + However 0.006 and 0.002 are considered different (returns 1) because + they respectively round to 0.01 and 0.0, even though + 0.006-0.002 = 0.004 which would be considered zero at 2 digits precision. + + + Precision must be given by ``precision_digits`` or ``precision_rounding``, + not both! :param int precision_digits: number of decimal digits to round to. :param float precision_rounding: decimal number representing the minimum @@ -1272,9 +1286,10 @@ def float_compare(amount1, amount2, precision_digits=None, precision_rounding=No """ rounding_factor = _float_check_precision(precision_digits=precision_digits, precision_rounding=precision_rounding) + amount1 = float_round(amount1, precision_rounding=rounding_factor) + amount2 = float_round(amount2, precision_rounding=rounding_factor) delta = amount1 - amount2 if float_is_zero(delta, precision_rounding=rounding_factor): return 0 - delta = float_round(delta, precision_rounding=rounding_factor) - return -1 if delta < 0 else 1 + return -1 if delta < 0.0 else 1 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file From ba2b2bdae168ef5d5451798925bfcb5df1a08d7b Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 15 Nov 2011 12:36:05 +0100 Subject: [PATCH 03/58] [IMP] tools.float_*: more tests with negative values, some cleanup bzr revid: odo@openerp.com-20111115113605-l5e6gg675qbrvjz6 --- openerp/addons/base/test/base_test.yml | 122 +++++++++++++++++-------- openerp/tools/misc.py | 48 +++++----- 2 files changed, 110 insertions(+), 60 deletions(-) diff --git a/openerp/addons/base/test/base_test.yml b/openerp/addons/base/test/base_test.yml index 9b686ccb0d3..56d723bf8a3 100644 --- a/openerp/addons/base/test/base_test.yml +++ b/openerp/addons/base/test/base_test.yml @@ -145,61 +145,111 @@ self.pool._init = True - - "Float precision tests: verify that float rounding methods are working correctly" + "Float precision tests: verify that float rounding methods are working correctly via res.currency" - !python {model: res.currency}: | currency = self.browse(cr, uid, ref('base.EUR')) - def try_round(self, cr, currency, amount, expected): + def try_round(amount, expected, self=self, cr=cr, currency=currency): result = str(self.round(cr, 1, currency, amount)) assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected) - try_round(self, cr, currency, 2.674,'2.67') - try_round(self, cr, currency, 2.675,'2.68') # in Python 2.7.2, round(2.675,2) gives 2.67 - try_round(self, cr, currency, 0.001,'0.0') - try_round(self, cr, currency, 0.0049,'0.0') # 0.0049 is closer to 0 than to 0.01, so should round down - try_round(self, cr, currency, 0.005,'0.01') # the rule is to round half up + try_round(2.674,'2.67') + try_round(2.675,'2.68') # in Python 2.7.2, round(2.675,2) gives 2.67 + try_round(-2.675,'-2.68') # in Python 2.7.2, round(2.675,2) gives 2.67 + try_round(0.001,'0.0') + try_round(-0.001,'-0.0') + try_round(0.0049,'0.0') # 0.0049 is closer to 0 than to 0.01, so should round down + try_round(0.005,'0.01') # the rule is to round half away from zero + try_round(-0.005,'-0.01') # the rule is to round half away from zero - def try_zero(self, cr, currency, amount, expected): + def try_zero(amount, expected, self=self, cr=cr, currency=currency): assert self.is_zero(cr, 1, currency, amount) == expected, "Rounding error: %s should be zero!" % amount - try_zero(self, cr, currency, 0.01, False) - try_zero(self, cr, currency, 0.001, True) - try_zero(self, cr, currency, 0.0046, True) - try_zero(self, cr, currency, 2.68-2.675, False) # 2.68 - 2.675 = 0.005 -> rounds to 0.01 - try_zero(self, cr, currency, 2.68-2.676, True) # 2.68 - 2.675 = 0.004 -> rounds to 0.0 + try_zero(0.01, False) + try_zero(-0.01, False) + try_zero(0.001, True) + try_zero(-0.001, True) + try_zero(0.0046, True) + try_zero(-0.0046, True) + try_zero(2.68-2.675, False) # 2.68 - 2.675 = 0.005 -> rounds to 0.01 + try_zero(2.68-2.676, True) # 2.68 - 2.675 = 0.004 -> rounds to 0.0 + try_zero(2.676-2.68, True) # 2.675 - 2.68 = -0.004 -> rounds to -0.0 + try_zero(2.675-2.68, False) # 2.675 - 2.68 = -0.005 -> rounds to -0.01 - def try_compare(self, cr, currency, amount1, amount2, expected): + def try_compare(amount1, amount2, expected, self=self, cr=cr, currency=currency): assert self.compare_amounts(cr, 1, currency, amount1, amount2) == expected, \ "Rounding error, compare_amounts(%s,%s) should be %s" % (amount1, amount2, expected) - try_compare(self, cr, currency, 0.001, 0.001, 0) - try_compare(self, cr, currency, 0.001, 0.002, 0) - try_compare(self, cr, currency, 2.675, 2.68, 0) - try_compare(self, cr, currency, 2.676, 2.68, 0) - try_compare(self, cr, currency, 2.674, 2.68, -1) - try_compare(self, cr, currency, 3, 2.68, 1) - try_compare(self, cr, currency, 0.01, 0, 1) + try_compare(0.001, 0.001, 0) + try_compare(-0.001, -0.001, 0) + try_compare(0.001, 0.002, 0) + try_compare(-0.001, -0.002, 0) + try_compare(2.675, 2.68, 0) + try_compare(2.676, 2.68, 0) + try_compare(-2.676, -2.68, 0) + try_compare(2.674, 2.68, -1) + try_compare(-2.674, -2.68, 1) + try_compare(3, 2.68, 1) + try_compare(-3, -2.68, -1) + try_compare(0.01, 0, 1) + try_compare(-0.01, 0, -1) +- + "Float precision tests: verify that float rounding methods are working correctly via tools" +- + !python {model: res.currency}: | from tools import float_compare, float_is_zero, float_round - def try_round_digits(float_round, amount, expected): - result = str(float_round(amount, precision_digits=3)) + def try_round(amount, expected, precision_digits=3, float_round=float_round): + result = str(float_round(amount, precision_digits=precision_digits)) assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected) - try_round_digits(float_round, 2.6745, '2.675') - try_round_digits(float_round, 2.6744, '2.674') - try_round_digits(float_round, 0.0004, '0.0') + try_round(2.6745, '2.675') + try_round(-2.6745, '-2.675') + try_round(2.6744, '2.674') + try_round(-2.6744, '-2.674') + try_round(0.0004, '0.0') + try_round(-0.0004, '-0.0') + #try_round(357.4555, '357.456') + #try_round(-357.4555, '-357.456') + try_round(457.4554, '457.455') + try_round(-457.4554, '-457.455') - def try_zero_digits(float_is_zero, amount, expected): + + def try_zero(amount, expected, float_is_zero=float_is_zero): assert float_is_zero(amount, precision_digits=3) == expected, "Rounding error: %s should be zero!" % amount - try_zero_digits(float_is_zero, 0.0002, True) - try_zero_digits(float_is_zero, 0.00034, True) - try_zero_digits(float_is_zero, 0.0005, False) - try_zero_digits(float_is_zero, 0.0008, False) + try_zero(0.0002, True) + try_zero(-0.0002, True) + try_zero(0.00034, True) + try_zero(0.0005, False) + try_zero(-0.0005, False) + try_zero(0.0008, False) + try_zero(-0.0008, False) - def try_compare_digits(float_compare, amount1, amount2, expected): + def try_compare(amount1, amount2, expected, float_compare=float_compare): assert float_compare(amount1, amount2, precision_digits=3) == expected, \ "Rounding error, compare_amounts(%s,%s) should be %s" % (amount1, amount2, expected) - try_compare_digits(float_compare, 0.0003, 0.0004, 0) - try_compare_digits(float_compare, 0.0002, 0.0005, -1) - try_compare_digits(float_compare, 0.0009, 0.0004, 1) + try_compare(0.0003, 0.0004, 0) + try_compare(-0.0003, -0.0004, 0) + try_compare(0.0002, 0.0005, -1) + try_compare(-0.0002, -0.0005, 1) + try_compare(0.0009, 0.0004, 1) + try_compare(-0.0009, -0.0004, -1) + try_compare(557.4555, 557.4556, 0) + try_compare(-557.4555, -557.4556, 0) + try_compare(657.4444, 657.445, -1) + try_compare(-657.4444, -657.445, 1) - # specifying 2 precisions is illegal: + # Rounding to different precision_roundings + def try_round(amount, expected, precision_rounding=None, float_round=float_round): + result = str(float_round(amount, precision_rounding=precision_rounding)) + assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected) + try_round(-457.4554, '-457.45', precision_rounding=0.05) + try_round(457.444, '457.5', precision_rounding=0.5) + try_round(457.3, '455.0', precision_rounding=5) + try_round(457.5, '460.0', precision_rounding=5) + try_round(457.1, '456.0', precision_rounding=3) + +- + "Float precision tests: verify that invalid parameters are forbidden" +- + !python {model: res.currency}: | + from tools import float_compare, float_is_zero, float_round try: float_is_zero(0.01, precision_digits=3, precision_rounding=0.01) except AssertionError: diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index 496767f7c41..5b5713749ac 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -1208,9 +1208,9 @@ def _float_check_precision(precision_digits=None, precision_rounding=None): return 10 ** -precision_digits return precision_rounding -def float_round(amount, precision_digits=None, precision_rounding=None): - """Return ``amount`` rounded to ``precision_digits`` - decimal digits, minimizing IEEE-854 floating point representation +def float_round(value, precision_digits=None, precision_rounding=None): + """Return ``value`` rounded to ``precision_digits`` + decimal digits, minimizing IEEE-754 floating point representation errors. Precision must be given by ``precision_digits`` or ``precision_rounding``, not both! @@ -1223,7 +1223,7 @@ def float_round(amount, precision_digits=None, precision_rounding=None): >>> round(2.675,2) 2.67 - :param float amount: the amount to round + :param float value: the value to round :param int precision_digits: number of decimal digits to round to. :param float precision_rounding: decimal number representing the minimum non-zero value at the desired precision (for example, 0.01 for a @@ -1233,18 +1233,18 @@ def float_round(amount, precision_digits=None, precision_rounding=None): rounding_factor = _float_check_precision(precision_digits=precision_digits, precision_rounding=precision_rounding) if rounding_factor == 0: return 0.0 - # /!\ First member below must be rounded to full unit! - # Do not pass rounding digits to round()! - return round(amount / rounding_factor) * rounding_factor -def float_is_zero(amount, precision_digits=None, precision_rounding=None): - """Returns true if ``amount`` is small enough to be treated as + # Then round to integer wrt. rounding factor + return round(value / rounding_factor) * rounding_factor + +def float_is_zero(value, precision_digits=None, precision_rounding=None): + """Returns true if ``value`` is small enough to be treated as zero at the given precision. Precision must be given by ``precision_digits`` or ``precision_rounding``, not both! - Warning: ``float_is_zero(amount1-amount2)`` is not always equivalent to - ``float_compare(amount1,amount2) == 0``, as the former will round after + Warning: ``float_is_zero(value1-value2)`` is not always equivalent to + ``float_compare(value1,value2) == 0``, as the former will round after computing the difference, while the latter will round before, giving different results for e.g. 0.006 and 0.002 at 2 digits precision. @@ -1252,16 +1252,16 @@ def float_is_zero(amount, precision_digits=None, precision_rounding=None): :param float precision_rounding: decimal number representing the minimum non-zero value at the desired precision (for example, 0.01 for a 2-digit precision). - :param float amount: amount to compare with currency's zero - :return: True if ``amount`` is considered 0 + :param float value: value to compare with currency's zero + :return: True if ``value`` is considered 0 """ rounding_factor = _float_check_precision(precision_digits=precision_digits, precision_rounding=precision_rounding) - return abs(float_round(amount, precision_rounding=rounding_factor)) < rounding_factor + return abs(float_round(value, precision_rounding=rounding_factor)) < rounding_factor -def float_compare(amount1, amount2, precision_digits=None, precision_rounding=None): - """Compare ``amount1`` and ``amount2`` after rounding them according to the - given precision. An amount is considered lower/greater than another amount +def float_compare(value1, value2, precision_digits=None, precision_rounding=None): + """Compare ``value1`` and ``value2`` after rounding them according to the + given precision. A value is considered lower/greater than another value if their rounded value is different. This is not the same as having a non-zero difference! @@ -1279,16 +1279,16 @@ def float_compare(amount1, amount2, precision_digits=None, precision_rounding=No :param float precision_rounding: decimal number representing the minimum non-zero value at the desired precision (for example, 0.01 for a 2-digit precision). - :param float amount1: first amount to compare - :param float amount2: second amount to compare - :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than, - equal to, or greater than ``amount2``, at the given precision. + :param float value1: first value to compare + :param float value2: second value to compare + :return: (resp.) -1, 0 or 1, if ``value1`` is (resp.) lower than, + equal to, or greater than ``value2``, at the given precision. """ rounding_factor = _float_check_precision(precision_digits=precision_digits, precision_rounding=precision_rounding) - amount1 = float_round(amount1, precision_rounding=rounding_factor) - amount2 = float_round(amount2, precision_rounding=rounding_factor) - delta = amount1 - amount2 + value1 = float_round(value1, precision_rounding=rounding_factor) + value2 = float_round(value2, precision_rounding=rounding_factor) + delta = value1 - value2 if float_is_zero(delta, precision_rounding=rounding_factor): return 0 return -1 if delta < 0.0 else 1 From 2aea3b278a67d567615f66dd72a73469abe14d7f Mon Sep 17 00:00:00 2001 From: "Vaibhav (OpenERP)" Date: Wed, 23 Nov 2011 17:56:35 +0530 Subject: [PATCH 04/58] [FIX] Dialog popup title in m2m,o2m,m2m. lp bug: https://launchpad.net/bugs/890205 fixed bzr revid: vda@tinyerp.com-20111123122635-d2xmyfshday933o3 --- addons/web/static/src/js/view_form.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index ced99bea7c2..7b808d897d6 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -1749,7 +1749,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ return; } var pop = new openerp.web.form.FormOpenPopup(self.view); - pop.show_element(self.field.relation, self.value[0],self.build_context(), {}); + pop.show_element(self.field.relation, self.value[0],self.build_context(), {title: self.string || self.name}); pop.on_write_completed.add_last(function() { self.set_value(self.value[0]); }); @@ -1910,6 +1910,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ var self = this; var pop = new openerp.web.form.SelectCreatePopup(this); pop.select_element(self.field.relation,{ + title: this.string || this.name, initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined, initial_view: view, disable_multiple_selection: true @@ -2333,6 +2334,7 @@ openerp.web.form.One2ManyListView = openerp.web.ListView.extend({ var pop = new openerp.web.form.SelectCreatePopup(this); pop.on_default_get.add(self.dataset.on_default_get); pop.select_element(self.o2m.field.relation,{ + title: self.name, initial_view: "form", alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, create_function: function(data, callback, error_callback) { @@ -2356,6 +2358,7 @@ openerp.web.form.One2ManyListView = openerp.web.ListView.extend({ var self = this; var pop = new openerp.web.form.FormOpenPopup(self.o2m.view); pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(),{ + title: self.name, auto_write: false, alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, parent_view: self.o2m.view, @@ -2477,7 +2480,7 @@ openerp.web.form.Many2ManyDataSet = openerp.web.DataSetStatic.extend({ openerp.web.form.Many2ManyListView = openerp.web.ListView.extend(/** @lends openerp.web.form.Many2ManyListView# */{ do_add_record: function () { var pop = new openerp.web.form.SelectCreatePopup(this); - pop.select_element(this.model, {}, + pop.select_element(this.model, {title: this.name}, new openerp.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]), this.m2m_field.build_context()); var self = this; @@ -2494,7 +2497,7 @@ openerp.web.form.Many2ManyListView = openerp.web.ListView.extend(/** @lends open do_activate_record: function(index, id) { var self = this; var pop = new openerp.web.form.FormOpenPopup(this); - pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {}); + pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {title: this.name}); pop.on_write_completed.add_last(function() { self.reload_content(); }); @@ -2533,7 +2536,7 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope this.render_element(); openerp.web.form.dialog(this.$element, {close:function() { self.check_exit(); - }}); + }, title: options.title || ""}); this.start(); }, start: function() { @@ -2720,7 +2723,7 @@ openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend(/** @lends openerp this.context = context || {}; this.options = _.defaults(options || {}, {"auto_write": true}); this.render_element(); - this.$element.dialog({title: '', + this.$element.dialog({title: options.title || '', modal: true, width: 960, height: 600}); From 663030a591c018ddabbbf105c4993830ff611ddf Mon Sep 17 00:00:00 2001 From: "Turkesh Patel (Open ERP)" Date: Tue, 29 Nov 2011 15:22:34 +0530 Subject: [PATCH 05/58] [FIX] solved problem of duplication in apprisal survey in hr_evaluation module bzr revid: tpa@tinyerp.com-20111129095234-p0d7u8ihfhp706k2 --- addons/hr_evaluation/hr_evaluation.py | 7 +++---- addons/hr_evaluation/hr_evaluation_view.xml | 2 -- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/addons/hr_evaluation/hr_evaluation.py b/addons/hr_evaluation/hr_evaluation.py index e1d451c9f7f..54a15d5593c 100644 --- a/addons/hr_evaluation/hr_evaluation.py +++ b/addons/hr_evaluation/hr_evaluation.py @@ -250,13 +250,12 @@ class hr_evaluation(osv.osv): return True def button_cancel(self, cr, uid, ids, context=None): + interview_obj=self.pool.get('hr.evaluation.interview') + evaluation = self.browse(cr, uid, ids[0], context) + interview_obj.survey_req_cancel(cr, uid, [r.id for r in evaluation.survey_request_ids]) self.write(cr, uid, ids,{'state':'cancel'}, context=context) return True - def button_draft(self, cr, uid, ids, context=None): - self.write(cr, uid, ids,{'state': 'draft'}, context=context) - return True - def write(self, cr, uid, ids, vals, context=None): if 'date' in vals: new_vals = {'date_deadline': vals.get('date')} diff --git a/addons/hr_evaluation/hr_evaluation_view.xml b/addons/hr_evaluation/hr_evaluation_view.xml index d8dfa7e40ca..4bf3ba0fff7 100644 --- a/addons/hr_evaluation/hr_evaluation_view.xml +++ b/addons/hr_evaluation/hr_evaluation_view.xml @@ -207,8 +207,6 @@ icon="gtk-execute"/> ' ].join('') } - if (!row_data[column.id]) { return value_if_empty === undefined ? '' : value_if_empty; } diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index 472001390e8..c8469eefe4d 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -557,6 +557,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# * @param {Function} callback should be called after the action is executed, if non-null */ do_button_action: function (name, id, callback) { + var self = this; var action = _.detect(this.columns, function (field) { return field.name === name; }); @@ -575,7 +576,9 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# c.add(action.context); } action.context = c; - this.do_execute_action(action, this.dataset, id, callback); + this.do_execute_action(action, this.dataset, id, function(){ + self.o2m.view.reload(); + }); }, /** * Handles the activation of a record (clicking on it) From ed53ef476137c707245e3ef50ab800f31de58aeb Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 20 Dec 2011 17:34:20 +0100 Subject: [PATCH 12/58] [IMP] float_utils: fix HALF_UP rounding according to discussions on bug 882036 Also improved test and added warning logging when crossing the precision limit due to too many significant digits. There is apparently a limitation in Python's float implementation for this. bzr revid: odo@openerp.com-20111220163420-lz0sh1h0yjkh6jdc --- openerp/addons/base/test/base_test.yml | 21 +++++- openerp/tools/float_utils.py | 89 ++++++++++++++++++++++++-- 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/openerp/addons/base/test/base_test.yml b/openerp/addons/base/test/base_test.yml index 56d723bf8a3..41c5c8d84e8 100644 --- a/openerp/addons/base/test/base_test.yml +++ b/openerp/addons/base/test/base_test.yml @@ -205,11 +205,28 @@ try_round(-2.6744, '-2.674') try_round(0.0004, '0.0') try_round(-0.0004, '-0.0') - #try_round(357.4555, '357.456') - #try_round(-357.4555, '-357.456') + try_round(357.4555, '357.456') + try_round(-357.4555, '-357.456') try_round(457.4554, '457.455') try_round(-457.4554, '-457.455') + # Extended float range test, inspired by Cloves Almeida's test on bug #882036. + fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555] + expecteds = ['.0', '.02', '.01', '.68', '.67', '.46', '.456', '.4556'] + precisions = [2, 2, 2, 2, 2, 2, 3, 4] + # We can't go higher than 12 significant digits with a fractional part, so + # that means max magnitude = 5 with precision 4 if sample values (x) go up to 10000 + # because that gives 5 + 4 + 4 = 13 significant digits. + for magnitude in range(5): + for i in xrange(len(fractions)): + frac, exp, prec = fractions[i], expecteds[i], precisions[i] + for sign in [-1,1]: + for x in xrange(0,10000,34): + n = x * 10**magnitude + f = sign * (n + frac) + f_exp = ('-' if f != 0 and sign == -1 else '') + str(n) + exp + try_round(f, f_exp, precision_digits=prec) + def try_zero(amount, expected, float_is_zero=float_is_zero): assert float_is_zero(amount, precision_digits=3) == expected, "Rounding error: %s should be zero!" % amount diff --git a/openerp/tools/float_utils.py b/openerp/tools/float_utils.py index effd473e269..202de89d0cf 100644 --- a/openerp/tools/float_utils.py +++ b/openerp/tools/float_utils.py @@ -19,6 +19,16 @@ # ############################################################################## +import logging +import math +from decimal import Decimal, ROUND_HALF_UP + +# When a number crosses this threshold, significant decimal +# digits may be lost when trying to render the float value, due to +# Python's float implementation. +# e.g. str(10060000.45556) == '10060000.4556' => lost 1 digit! +SIGNIFICANT_DIGITS_SCALE_LIMIT = math.log(10**12, 2) # 10**12 ~= 2**39.86 + def _float_check_precision(precision_digits=None, precision_rounding=None): assert (precision_digits is not None or precision_rounding is not None) and \ not (precision_digits and precision_rounding),\ @@ -51,10 +61,39 @@ def float_round(value, precision_digits=None, precision_rounding=None): """ rounding_factor = _float_check_precision(precision_digits=precision_digits, precision_rounding=precision_rounding) - if rounding_factor == 0: return 0.0 - - # Then round to integer wrt. rounding factor - return round(value / rounding_factor) * rounding_factor + if rounding_factor == 0 or value == 0: return 0.0 + # scale up by rounding_factor, in order to implement rounding to arbitrary + # `units` or 'rounding_factors'. + # Example: if rounding_factor is 0.5, 1.3 should round to 1.5 + # So we'll do this: scaled_value = 1.3 / 0.5 = 2.6 + # int_rounded = round(2.6) = 3 + # result = 3 * 0.5 = 1.5 + # Also, .5 is a binary fraction, so this automatically solves some tricky + # cases when rounding_factor is a negative power of 10. E.g 2.6745 is + # difficult to round to 0.001 because it does not have an exact IEEE754 + # representation, but 2674.5 is simple to round to 2675 because both + # are exactly represented. + scaled_value = value / rounding_factor + # Despite the advantage of rounding to .5 binary fractions, we still need + # to add a small epsilon value to take care of cases where the float repr + # is slightly too far below .5 to properly round *up* automatically. + # That epsilon needs to be scaled according to the order of magnitude of + # the value. (Credit: discussed with several community members on bug 882036) + epsilon_scale = math.log(abs(scaled_value), 2) + frac_part, _ = math.modf(scaled_value) + if frac_part and epsilon_scale > SIGNIFICANT_DIGITS_SCALE_LIMIT: + print 'Float rounding of %r to %r precision requires too many '\ + 'significant digits, a loss of precision may occur in the '\ + 'least significant digits' % (value,rounding_factor) + logging.getLogger('float_utils')\ + .warning('Float rounding of %r to %r precision requires too many ' + 'significant digits, a loss of precision may occur in the ' + 'least significant digits', value, rounding_factor) + epsilon = 2**(epsilon_scale-50) + scaled_value += cmp(scaled_value,0) * epsilon + rounded_value = round(scaled_value) + result = rounded_value * rounding_factor + return result def float_is_zero(value, precision_digits=None, precision_rounding=None): """Returns true if ``value`` is small enough to be treated as @@ -114,4 +153,44 @@ def float_compare(value1, value2, precision_digits=None, precision_rounding=None value2 = float_round(value2, precision_rounding=rounding_factor) delta = value1 - value2 if float_is_zero(delta, precision_rounding=rounding_factor): return 0 - return -1 if delta < 0.0 else 1 \ No newline at end of file + return -1 if delta < 0.0 else 1 + + + + + +if __name__ == "__main__": + + import time + start = time.time() + count = 0 + errors = 0 + + def try_round(amount, expected, precision_digits=3): + global count, errors; count += 1 + result = float_round(amount, precision_digits=precision_digits) + if str(result) != expected: + errors += 1 + print '###!!! Rounding error: got %s or %s, expected %s' % (str(result), repr(result), expected) + + # Extended float range test, inspired by Cloves Almeida's test on bug #882036. + fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555] + expecteds = ['.0', '.02', '.01', '.68', '.67', '.46', '.456', '.4556'] + precisions = [2, 2, 2, 2, 2, 2, 3, 4] + for magnitude in range(5): + for i in xrange(len(fractions)): + frac, exp, prec = fractions[i], expecteds[i], precisions[i] + for sign in [-1,1]: + for x in xrange(0,10000,17): + n = x * 10**magnitude + f = sign * (n + frac) + f_exp = ('-' if f != 0 and sign == -1 else '') + str(n) + exp + try_round(f, f_exp, precision_digits=prec) + + stop = time.time() + + # Micro-bench results: + # 47130 round calls in 0.422306060791 secs, with Python 2.6.7 on Core i3 x64 + # with decimal: + # 47130 round calls in 6.612248100021 secs, with Python 2.6.7 on Core i3 x64 + print count, " round calls, ", errors, "errors, done in ", (stop-start), 'secs' From 707bf0d8aa55a4c493d1a94393f38a1799233b96 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 20 Dec 2011 18:03:28 +0100 Subject: [PATCH 13/58] [FIX] fields.float: properly round when coercing to desired float precision lp bug: https://launchpad.net/bugs/882036 fixed bzr revid: odo@openerp.com-20111220170328-nvutckcxnitkcq9d --- openerp/osv/fields.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 42bc9b75c1d..0579677fd5e 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -45,6 +45,7 @@ import openerp import openerp.netsvc as netsvc import openerp.tools as tools from openerp.tools.translate import _ +from openerp.tools import float_round def _symbol_set(symb): if symb == None or symb == False: @@ -229,17 +230,17 @@ class float(_column): def __init__(self, string='unknown', digits=None, digits_compute=None, required=False, **args): _column.__init__(self, string=string, required=required, **args) self.digits = digits + # synopsis: digits_compute(cr) -> (precision, scale) self.digits_compute = digits_compute if required: warnings.warn("Making a float field `required` has no effect, as NULL values are " "automatically turned into 0.0", PendingDeprecationWarning, stacklevel=2) - def digits_change(self, cr): if self.digits_compute: - t = self.digits_compute(cr) - self._symbol_set=('%s', lambda x: ('%.'+str(t[1])+'f') % (__builtin__.float(x or 0.0),)) - self.digits = t + self.digits = self.digits_compute(cr) + precision, scale = self.digits + self._symbol_set = ('%s', lambda x: str(float_round(__builtin__.float(x or 0.0), precision_digits=scale))) class date(_column): _type = 'date' @@ -991,10 +992,9 @@ class function(_column): def digits_change(self, cr): if self.digits_compute: - t = self.digits_compute(cr) - self._symbol_set=('%s', lambda x: ('%.'+str(t[1])+'f') % (__builtin__.float(x or 0.0),)) - self.digits = t - + self.digits = self.digits_compute(cr) + precision, scale = self.digits + self._symbol_set = ('%s', lambda x: str(float_round(__builtin__.float(x or 0.0), precision_digits=scale))) def search(self, cr, uid, obj, name, args, context=None): if not self._fnct_search: From 43435fb74e0b18b569362088e38166e4fdebf28d Mon Sep 17 00:00:00 2001 From: Minh Tran Date: Tue, 20 Dec 2011 18:42:29 +0100 Subject: [PATCH 14/58] Restyling of the login page bzr revid: mit@openerp.com-20111220174229-i615k9nhuoggwl3u --- addons/web/static/src/css/base.css | 185 +++++++++++++++++++++++--- addons/web/static/src/img/logo2.png | Bin 0 -> 8836 bytes addons/web/static/src/img/pattern.png | Bin 0 -> 98 bytes addons/web/static/src/xml/base.xml | 134 +++++++++---------- 4 files changed, 230 insertions(+), 89 deletions(-) create mode 100644 addons/web/static/src/img/logo2.png create mode 100644 addons/web/static/src/img/pattern.png diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 25e593170c6..51b6d0e26dc 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -88,12 +88,6 @@ body.openerp, .openerp textarea, .openerp input, .openerp select, .openerp optio .openerp .login { display: none; } -.openerp .login form { - float: left; - width: 420px; - margin-left: 40px; - margin-bottom: 60px; -} .openerp .login fieldset { padding-bottom: 5px; min-width: 100px; @@ -133,19 +127,27 @@ body.openerp, .openerp textarea, .openerp input, .openerp select, .openerp optio } .openerp .login .login_error_message { display: none; - background-color: #9A0404; - border-radius: 3px; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - color: white; + background-color: #b41616; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.8); + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.8); + -box-shadow: 0 1px 4px rgba(0, 0, 0, 0.8); + filter: alpha(opacity=95); + -khtml-opacity: 0.95; + -moz-opacity: 0.95; + color: #eee; font-size: 16px; - font-weight: bold; - padding: 5px; - margin-top: 5px; + padding: 8px 12px; + margin-top: 15px; + text-align: center; +} +.openerp .login .login_invalid { text-align: center; } .openerp .login .login_invalid .login_error_message { - display: block; + display: inline-block; } .openerp.login-mode .login-container { @@ -157,6 +159,8 @@ body.openerp, .openerp textarea, .openerp input, .openerp select, .openerp optio .openerp.login-mode .menu, .openerp.login-mode .secondary_menu, .openerp.login-mode .oe-application, +.openerp.login-mode .oe_footer, +.openerp.login-mode .header, .openerp.login-mode .db_options_row { display: none; } @@ -1841,3 +1845,154 @@ ul.oe-arrow-list li.oe-arrow-list-selected .oe-arrow-list-after { cursor: default; text-shadow: 0 1px 1px white !important; } + +/* Login page */ + +#oe_login { + padding: 0; + margin: 0; + font-family: "Lucida Grande", Helvetica, Verdana, Arial; + background: url("/web/static/src/img/pattern.png") repeat; + color: #eee; + font-size: 14px; + height: 100%; +} + +#oe_login ul, ol { + padding: 0; + margin: 0; +} + +#oe_login li { + list-style-type: none; + padding-bottom: 4px; +} + +#oe_login a { + color: #eee; + text-decoration: none; +} + +#oe_login button { + float: right; + display: inline-block; + cursor: pointer; + padding: 6px 16px; + font-size: 13px; + font-family: "Lucida Grande", Helvetica, Verdana, Arial; + border: 1px solid #222222; + color: white; + margin: 0; + background: #600606; + background: -moz-linear-gradient(#b92020, #600606); + background: -webkit-gradient(linear, left top, left bottom, from(#b92020), to(#600606)); + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(155, 155, 155, 0.4) inset; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(155, 155, 155, 0.4) inset; + -o-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(155, 155, 155, 0.4) inset; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(155, 155, 155, 0.4) inset; +} + +#oe_login input, #oe_login select { + width: 250px; + margin-bottom: 10px; + font-size: 14px; + padding: 5px 6px; + border: 1px solid #999999; + background: whitesmoke; + -moz-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.3); + -webkit-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.3); + -box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.3); + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; +} + +#oe_login .dbpane { + position: fixed; + top: 0; + right: 8px; + padding: 6px 12px 0px; + color: #eee; + border: solid 1px #333; + background: #101010; + filter: alpha(opacity=95); + -khtml-opacity: 0.95; + -moz-opacity: 0.95; + opacity: 0.95; + -moz-border-radius: 0 0 8px 8px; + -webkit-border-radius: 0 0 8px 8px; + border-radius: 0 0 8px 8px; +} + +#oe_login .bottom { + position: absolute; + top: 50%; + left: 0; + right: 0; + bottom: 0; + text-shadow: 0 1px 1px #999999; + background: #600606; + background: -moz-linear-gradient(#b41616, #600606); + background: -webkit-gradient(linear, left top, left bottom, from(#b41616), to(#600606)); +} + +#oe_login .pane { + position: absolute; + top: 50%; + left: 50%; + margin: -160px -166px; + border: solid 1px #333333; + background: #101010; + padding: 20px 32px; + text-align: left; + -moz-border-radius: 8px; + -webkit-border-radius: 8px; + border-radius: 8px; + -moz-box-shadow: 0 0 18px rgba(0, 0, 0, 0.9); + -webkit-box-shadow: 0 0 18px rgba(0, 0, 0, 0.9); + -box-shadow: 0 0 18px rgba(0, 0, 0, 0.9); + filter: alpha(opacity=95); + -khtml-opacity: 0.95; + -moz-opacity: 0.95; + opacity: 0.95; +} + +#oe_login .pane h2 { + margin-top: 0; + font-size: 18px; +} + +#oe_login #logo { + position: absolute; + top: -70px; + left: 0; + width: 100%; + margin: 0 auto; + text-align: center; +} + +#oe_login .footer { + position: absolute; + bottom: -40px; + left: 0; + width: 100%; + text-align: center; +} + +#oe_login .footer a { + font-size: 13px; + margin: 0 8px; +} + +#oe_login .footer a:hover { + text-decoration: underline; +} + +#oe_login .openerp { + font-weight: bold; + font-family: serif; + font-size: 16px; +} diff --git a/addons/web/static/src/img/logo2.png b/addons/web/static/src/img/logo2.png new file mode 100644 index 0000000000000000000000000000000000000000..49e789fa01dcc6f449f240e5dafb9cf7f558cfc8 GIT binary patch literal 8836 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
+ @@ -61,6 +62,41 @@
@@ -36,7 +37,7 @@
-
+
+ + + + + + + + + + + + + + + +
+
+ +
+
+
+
+
+ + + + + +
+
+
+
+
@@ -262,80 +298,30 @@ -
-
-
- - - -
- - - - - - - - - - - - - - - - - -
- -
- - -
-
-
- -
- +
From 07baf07cdc8d88e3b7228abe37bf28f8c223d608 Mon Sep 17 00:00:00 2001 From: Minh Tran Date: Tue, 20 Dec 2011 19:21:08 +0100 Subject: [PATCH 15/58] [FIX] Fixed the broken link to www.openerp.com bzr revid: mit@openerp.com-20111220182108-3ymlq6huif5tjc80 --- addons/web/static/src/xml/base.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index b327d3d2d61..64e72be3cc9 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -318,7 +318,7 @@
From 759ccd984542ce4077dfcc9fa14198b60719dded Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 21 Dec 2011 00:47:40 +0100 Subject: [PATCH 16/58] [IMP] float_utils: simplified code, added float_repr lp bug: https://launchpad.net/bugs/882036 fixed bzr revid: odo@openerp.com-20111220234740-kotcgoz3opcbkx4b --- openerp/addons/base/test/base_test.yml | 51 +++++----- openerp/osv/fields.py | 8 +- openerp/tools/float_utils.py | 128 ++++++++++++------------- 3 files changed, 92 insertions(+), 95 deletions(-) diff --git a/openerp/addons/base/test/base_test.yml b/openerp/addons/base/test/base_test.yml index 41c5c8d84e8..fcead834d93 100644 --- a/openerp/addons/base/test/base_test.yml +++ b/openerp/addons/base/test/base_test.yml @@ -148,16 +148,20 @@ "Float precision tests: verify that float rounding methods are working correctly via res.currency" - !python {model: res.currency}: | + from tools import float_repr + from math import log10 currency = self.browse(cr, uid, ref('base.EUR')) - def try_round(amount, expected, self=self, cr=cr, currency=currency): - result = str(self.round(cr, 1, currency, amount)) + def try_round(amount, expected, self=self, cr=cr, currency=currency, float_repr=float_repr, + log10=log10): + digits = max(0,-int(log10(currency.rounding))) + result = float_repr(self.round(cr, 1, currency, amount), precision_digits=digits) assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected) try_round(2.674,'2.67') try_round(2.675,'2.68') # in Python 2.7.2, round(2.675,2) gives 2.67 try_round(-2.675,'-2.68') # in Python 2.7.2, round(2.675,2) gives 2.67 - try_round(0.001,'0.0') - try_round(-0.001,'-0.0') - try_round(0.0049,'0.0') # 0.0049 is closer to 0 than to 0.01, so should round down + try_round(0.001,'0.00') + try_round(-0.001,'-0.00') + try_round(0.0049,'0.00') # 0.0049 is closer to 0 than to 0.01, so should round down try_round(0.005,'0.01') # the rule is to round half away from zero try_round(-0.005,'-0.01') # the rule is to round half away from zero @@ -195,16 +199,17 @@ "Float precision tests: verify that float rounding methods are working correctly via tools" - !python {model: res.currency}: | - from tools import float_compare, float_is_zero, float_round - def try_round(amount, expected, precision_digits=3, float_round=float_round): - result = str(float_round(amount, precision_digits=precision_digits)) + from tools import float_compare, float_is_zero, float_round, float_repr + def try_round(amount, expected, precision_digits=3, float_round=float_round, float_repr=float_repr): + result = float_repr(float_round(amount, precision_digits=precision_digits), + precision_digits=precision_digits) assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected) try_round(2.6745, '2.675') try_round(-2.6745, '-2.675') try_round(2.6744, '2.674') try_round(-2.6744, '-2.674') - try_round(0.0004, '0.0') - try_round(-0.0004, '-0.0') + try_round(0.0004, '0.000') + try_round(-0.0004, '-0.000') try_round(357.4555, '357.456') try_round(-357.4555, '-357.456') try_round(457.4554, '457.455') @@ -212,16 +217,15 @@ # Extended float range test, inspired by Cloves Almeida's test on bug #882036. fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555] - expecteds = ['.0', '.02', '.01', '.68', '.67', '.46', '.456', '.4556'] + expecteds = ['.00', '.02', '.01', '.68', '.67', '.46', '.456', '.4556'] precisions = [2, 2, 2, 2, 2, 2, 3, 4] - # We can't go higher than 12 significant digits with a fractional part, so - # that means max magnitude = 5 with precision 4 if sample values (x) go up to 10000 - # because that gives 5 + 4 + 4 = 13 significant digits. - for magnitude in range(5): + # Note: max precision for double floats is 53 bits of precision or + # 17 significant decimal digits + for magnitude in range(7): for i in xrange(len(fractions)): frac, exp, prec = fractions[i], expecteds[i], precisions[i] for sign in [-1,1]: - for x in xrange(0,10000,34): + for x in xrange(0,10000,97): n = x * 10**magnitude f = sign * (n + frac) f_exp = ('-' if f != 0 and sign == -1 else '') + str(n) + exp @@ -252,15 +256,16 @@ try_compare(657.4444, 657.445, -1) try_compare(-657.4444, -657.445, 1) - # Rounding to different precision_roundings - def try_round(amount, expected, precision_rounding=None, float_round=float_round): - result = str(float_round(amount, precision_rounding=precision_rounding)) + # Rounding to unusual rounding units (e.g. coin values) + def try_round(amount, expected, precision_rounding=None, float_round=float_round, float_repr=float_repr): + result = float_repr(float_round(amount, precision_rounding=precision_rounding), + precision_digits=2) assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected) try_round(-457.4554, '-457.45', precision_rounding=0.05) - try_round(457.444, '457.5', precision_rounding=0.5) - try_round(457.3, '455.0', precision_rounding=5) - try_round(457.5, '460.0', precision_rounding=5) - try_round(457.1, '456.0', precision_rounding=3) + try_round(457.444, '457.50', precision_rounding=0.5) + try_round(457.3, '455.00', precision_rounding=5) + try_round(457.5, '460.00', precision_rounding=5) + try_round(457.1, '456.00', precision_rounding=3) - "Float precision tests: verify that invalid parameters are forbidden" diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 0579677fd5e..ea1b6d93329 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -45,7 +45,7 @@ import openerp import openerp.netsvc as netsvc import openerp.tools as tools from openerp.tools.translate import _ -from openerp.tools import float_round +from openerp.tools import float_round, float_repr def _symbol_set(symb): if symb == None or symb == False: @@ -240,7 +240,8 @@ class float(_column): if self.digits_compute: self.digits = self.digits_compute(cr) precision, scale = self.digits - self._symbol_set = ('%s', lambda x: str(float_round(__builtin__.float(x or 0.0), precision_digits=scale))) + self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0), precision_digits=scale), + precision_digits=scale)) class date(_column): _type = 'date' @@ -994,7 +995,8 @@ class function(_column): if self.digits_compute: self.digits = self.digits_compute(cr) precision, scale = self.digits - self._symbol_set = ('%s', lambda x: str(float_round(__builtin__.float(x or 0.0), precision_digits=scale))) + self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0), precision_digits=scale), + precision_digits=scale)) def search(self, cr, uid, obj, name, args, context=None): if not self._fnct_search: diff --git a/openerp/tools/float_utils.py b/openerp/tools/float_utils.py index 202de89d0cf..5c934114e3c 100644 --- a/openerp/tools/float_utils.py +++ b/openerp/tools/float_utils.py @@ -19,15 +19,7 @@ # ############################################################################## -import logging import math -from decimal import Decimal, ROUND_HALF_UP - -# When a number crosses this threshold, significant decimal -# digits may be lost when trying to render the float value, due to -# Python's float implementation. -# e.g. str(10060000.45556) == '10060000.4556' => lost 1 digit! -SIGNIFICANT_DIGITS_SCALE_LIMIT = math.log(10**12, 2) # 10**12 ~= 2**39.86 def _float_check_precision(precision_digits=None, precision_rounding=None): assert (precision_digits is not None or precision_rounding is not None) and \ @@ -40,20 +32,12 @@ def _float_check_precision(precision_digits=None, precision_rounding=None): def float_round(value, precision_digits=None, precision_rounding=None): """Return ``value`` rounded to ``precision_digits`` decimal digits, minimizing IEEE-754 floating point representation - errors. + errors, and applying HALF-UP (away from zero) tie-breaking rule. Precision must be given by ``precision_digits`` or ``precision_rounding``, not both! - To illustrate how this is different from the default round() builtin, - here is an example (depends on Python version, here is for v2.7.2 x64):: - - >>> round_float(2.675) - 2.68 - >>> round(2.675,2) - 2.67 - :param float value: the value to round - :param int precision_digits: number of decimal digits to round to. + :param int precision_digits: number of fractional digits to round to. :param float precision_rounding: decimal number representing the minimum non-zero value at the desired precision (for example, 0.01 for a 2-digit precision). @@ -62,58 +46,51 @@ def float_round(value, precision_digits=None, precision_rounding=None): rounding_factor = _float_check_precision(precision_digits=precision_digits, precision_rounding=precision_rounding) if rounding_factor == 0 or value == 0: return 0.0 - # scale up by rounding_factor, in order to implement rounding to arbitrary - # `units` or 'rounding_factors'. - # Example: if rounding_factor is 0.5, 1.3 should round to 1.5 - # So we'll do this: scaled_value = 1.3 / 0.5 = 2.6 - # int_rounded = round(2.6) = 3 - # result = 3 * 0.5 = 1.5 - # Also, .5 is a binary fraction, so this automatically solves some tricky - # cases when rounding_factor is a negative power of 10. E.g 2.6745 is - # difficult to round to 0.001 because it does not have an exact IEEE754 - # representation, but 2674.5 is simple to round to 2675 because both - # are exactly represented. - scaled_value = value / rounding_factor - # Despite the advantage of rounding to .5 binary fractions, we still need - # to add a small epsilon value to take care of cases where the float repr - # is slightly too far below .5 to properly round *up* automatically. - # That epsilon needs to be scaled according to the order of magnitude of - # the value. (Credit: discussed with several community members on bug 882036) - epsilon_scale = math.log(abs(scaled_value), 2) - frac_part, _ = math.modf(scaled_value) - if frac_part and epsilon_scale > SIGNIFICANT_DIGITS_SCALE_LIMIT: - print 'Float rounding of %r to %r precision requires too many '\ - 'significant digits, a loss of precision may occur in the '\ - 'least significant digits' % (value,rounding_factor) - logging.getLogger('float_utils')\ - .warning('Float rounding of %r to %r precision requires too many ' - 'significant digits, a loss of precision may occur in the ' - 'least significant digits', value, rounding_factor) - epsilon = 2**(epsilon_scale-50) - scaled_value += cmp(scaled_value,0) * epsilon - rounded_value = round(scaled_value) - result = rounded_value * rounding_factor + + # NORMALIZE - ROUND - DENORMALIZE + # In order to easily support rounding to arbitrary 'steps' (e.g. coin values), + # we normalize the value before rounding it as an integer, and de-normalize + # after rounding: e.g. float_round(1.3, precision_rounding=.5) == 1.5 + + # TIE-BREAKING: HALF-UP + # We want to apply HALF-UP tie-breaking rules, i.e. 0.5 rounds away from 0. + # Due to IEE754 float/double representation limits, the approximation of the + # real value may be slightly below the tie limit, resulting in an error of + # 1 unit in the last place (ulp) after rounding. + # For example 2.675 == 2.6749999999999998. + # To correct this, we add a very small epsilon value, scaled to the + # the order of magnitude of the value, to tip the tie-break in the right + # direction. + # Credit: discussion with OpenERP community members on bug 882036 + + normalized_value = value / rounding_factor # normalize + epsilon_magnitude = math.log(abs(normalized_value), 2) + epsilon = 2**(epsilon_magnitude-53) + normalized_value += cmp(normalized_value,0) * epsilon + rounded_value = round(normalized_value) # round to integer + result = rounded_value * rounding_factor # de-normalize return result def float_is_zero(value, precision_digits=None, precision_rounding=None): """Returns true if ``value`` is small enough to be treated as - zero at the given precision (smaller than the given *epsilon*). + zero at the given precision (smaller than the corresponding *epsilon*). + The precision (``10**-precision_digits`` or ``precision_rounding``) + is used as the zero *epsilon*: values less than that are considered + to be zero. Precision must be given by ``precision_digits`` or ``precision_rounding``, - not both! Here the precision (``10**-precision_digits`` or - ``precision_rounding``) is used as the zero *epsilon*: values smaller - than that are considered to be zero. + not both! - Warning: ``float_is_zero(value1-value2)`` is not always equivalent to + Warning: ``float_is_zero(value1-value2)`` is not equivalent to ``float_compare(value1,value2) == 0``, as the former will round after computing the difference, while the latter will round before, giving different results for e.g. 0.006 and 0.002 at 2 digits precision. - :param int precision_digits: number of decimal digits to round to. + :param int precision_digits: number of fractional digits to round to. :param float precision_rounding: decimal number representing the minimum non-zero value at the desired precision (for example, 0.01 for a 2-digit precision). - :param float value: value to compare with currency's zero - :return: True if ``value`` is considered 0 + :param float value: value to compare with the precision's zero + :return: True if ``value`` is considered zero """ epsilon = _float_check_precision(precision_digits=precision_digits, precision_rounding=precision_rounding) @@ -129,16 +106,16 @@ def float_compare(value1, value2, precision_digits=None, precision_rounding=None Example: 1.432 and 1.431 are equal at 2 digits precision, so this method would return 0 - However 0.006 and 0.002 are considered different (method returns 1) because - they respectively round to 0.01 and 0.0, even though 0.006-0.002 = 0.004 - which would be considered zero at 2 digits precision. + However 0.006 and 0.002 are considered different (this method returns 1) + because they respectively round to 0.01 and 0.0, even though + 0.006-0.002 = 0.004 which would be considered zero at 2 digits precision. - Warning: ``float_is_zero(value1-value2)`` is not always equivalent to + Warning: ``float_is_zero(value1-value2)`` is not equivalent to ``float_compare(value1,value2) == 0``, as the former will round after computing the difference, while the latter will round before, giving different results for e.g. 0.006 and 0.002 at 2 digits precision. - :param int precision_digits: number of decimal digits to round to. + :param int precision_digits: number of fractional digits to round to. :param float precision_rounding: decimal number representing the minimum non-zero value at the desired precision (for example, 0.01 for a 2-digit precision). @@ -155,8 +132,20 @@ def float_compare(value1, value2, precision_digits=None, precision_rounding=None if float_is_zero(delta, precision_rounding=rounding_factor): return 0 return -1 if delta < 0.0 else 1 +def float_repr(value, precision_digits): + """Returns a string representation of a float with the + the given number of fractional digits. This should not be + used to perform a rounding operation (this is done via + :meth:`~.float_round`), but only to produce a suitable + string representation for a float. - + :param int precision_digits: number of fractional digits to + include in the output + """ + # Can't use str() here because it seems to have an intrisic + # rounding to 12 significant digits, which causes a loss of + # precision. e.g. str(123456789.1234) == str(123456789.123)!! + return ("%%.%sf" % precision_digits) % value if __name__ == "__main__": @@ -168,20 +157,21 @@ if __name__ == "__main__": def try_round(amount, expected, precision_digits=3): global count, errors; count += 1 - result = float_round(amount, precision_digits=precision_digits) - if str(result) != expected: + result = float_repr(float_round(amount, precision_digits=precision_digits), + precision_digits=precision_digits) + if result != expected: errors += 1 - print '###!!! Rounding error: got %s or %s, expected %s' % (str(result), repr(result), expected) + print '###!!! Rounding error: got %s , expected %s' % (result, expected) # Extended float range test, inspired by Cloves Almeida's test on bug #882036. fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555] - expecteds = ['.0', '.02', '.01', '.68', '.67', '.46', '.456', '.4556'] + expecteds = ['.00', '.02', '.01', '.68', '.67', '.46', '.456', '.4556'] precisions = [2, 2, 2, 2, 2, 2, 3, 4] - for magnitude in range(5): + for magnitude in range(7): for i in xrange(len(fractions)): frac, exp, prec = fractions[i], expecteds[i], precisions[i] for sign in [-1,1]: - for x in xrange(0,10000,17): + for x in xrange(0,10000,97): n = x * 10**magnitude f = sign * (n + frac) f_exp = ('-' if f != 0 and sign == -1 else '') + str(n) + exp From e410b81cc4adba82e5619849da802ecb7b3e44ec Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 21 Dec 2011 02:11:00 +0100 Subject: [PATCH 17/58] [FIX] fields.float: always round floats when persisting Added test to try a full round-trip to the database as well to make sure the automatic rounding is working properly. lp bug: https://launchpad.net/bugs/882036 fixed bzr revid: odo@openerp.com-20111221011100-gybt9pjdfpg4wc86 --- openerp/addons/base/test/base_test.yml | 20 ++++++++++++++++++++ openerp/osv/fields.py | 17 +++++++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/openerp/addons/base/test/base_test.yml b/openerp/addons/base/test/base_test.yml index fcead834d93..450719af046 100644 --- a/openerp/addons/base/test/base_test.yml +++ b/openerp/addons/base/test/base_test.yml @@ -267,6 +267,26 @@ try_round(457.5, '460.00', precision_rounding=5) try_round(457.1, '456.00', precision_rounding=3) +- + "Float precision tests: check that proper rounding is performed for float persistence" +- + !python {model: res.currency}: | + currency = self.browse(cr, uid, ref('base.EUR')) + res_currency_rate = self.pool.get('res.currency.rate') + from tools import float_compare, float_is_zero, float_round, float_repr + def try_roundtrip(value, expected, self=self, cr=cr, currency=currency, + res_currency_rate=res_currency_rate): + rate_id = res_currency_rate.create(cr, 1, {'name':'2000-01-01', + 'rate': value, + 'currency_id': currency.id}) + rate = res_currency_rate.read(cr, 1, rate_id, ['rate'])['rate'] + assert rate == expected, 'Roundtrip error: got %s back from db, expected %s' % (rate, expected) + # res.currency.rate uses 6 digits of precision by default + try_roundtrip(2.6748955, 2.674896) + try_roundtrip(-2.6748955, -2.674896) + try_roundtrip(10000.999999, 10000.999999) + try_roundtrip(-10000.999999, -10000.999999) + - "Float precision tests: verify that invalid parameters are forbidden" - diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index ea1b6d93329..5a40eaff5fa 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -239,8 +239,10 @@ class float(_column): def digits_change(self, cr): if self.digits_compute: self.digits = self.digits_compute(cr) + if self.digits: precision, scale = self.digits - self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0), precision_digits=scale), + self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0), + precision_digits=scale), precision_digits=scale)) class date(_column): @@ -992,11 +994,14 @@ class function(_column): self._symbol_set = integer._symbol_set def digits_change(self, cr): - if self.digits_compute: - self.digits = self.digits_compute(cr) - precision, scale = self.digits - self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0), precision_digits=scale), - precision_digits=scale)) + if self._type == 'float': + if self.digits_compute: + self.digits = self.digits_compute(cr) + if self.digits: + precision, scale = self.digits + self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0), + precision_digits=scale), + precision_digits=scale)) def search(self, cr, uid, obj, name, args, context=None): if not self._fnct_search: From 84a517a6d7520ffc1ead1fe58f29e4993bf8891f Mon Sep 17 00:00:00 2001 From: "Rucha (Open ERP)" Date: Wed, 21 Dec 2011 14:38:11 +0530 Subject: [PATCH 18/58] [IMP]: Removed pageSize from report rml as it will be taken from company paper format; bzr revid: rpa@tinyerp.com-20111221090811-tg1vbfqs303f8yvq --- addons/account/project/report/analytic_balance.rml | 2 +- addons/account/project/report/analytic_journal.rml | 2 +- addons/account/project/report/cost_ledger.rml | 2 +- addons/account/project/report/inverted_analytic_balance.rml | 2 +- addons/account/project/report/quantity_cost_ledger.rml | 2 +- addons/account/report/account_balance.rml | 2 +- addons/account/report/account_central_journal.rml | 2 +- addons/account/report/account_financial_report.rml | 2 +- addons/account/report/account_general_journal.rml | 2 +- addons/account/report/account_general_ledger.rml | 2 +- addons/account/report/account_journal.rml | 2 +- addons/account/report/account_partner_balance.rml | 2 +- addons/account/report/account_partner_ledger.rml | 2 +- addons/account/report/account_partner_ledger_other.rml | 2 +- addons/account/report/account_print_invoice.rml | 2 +- addons/account/report/account_print_overdue.rml | 2 +- addons/account/report/account_tax_report.rml | 2 +- addons/account_analytic_plans/report/crossovered_analytic.rml | 2 +- addons/account_budget/report/analytic_account_budget_report.rml | 2 +- addons/account_budget/report/budget_report.rml | 2 +- addons/account_budget/report/crossovered_budget_report.rml | 2 +- addons/account_followup/report/account_followup_print.rml | 2 +- .../report/report_account_invoice_layout.rml | 2 +- .../account_invoice_layout/report/special_message_invoice.rml | 2 +- addons/account_payment/report/order.rml | 2 +- addons/account_voucher/report/account_voucher.rml | 2 +- addons/account_voucher/report/account_voucher_print.rml | 2 +- addons/auction/report/auction_artists.rml | 2 +- addons/auction/report/auction_bids.rml | 2 +- addons/auction/report/auction_buyer_result.rml | 2 +- addons/auction/report/auction_catelog.rml | 2 +- addons/auction/report/auction_objects.rml | 2 +- addons/auction/report/auction_result.rml | 2 +- addons/auction/report/auction_total.rml | 2 +- addons/auction/report/auction_total_rml.rml | 2 +- addons/auction/report/bids_lots.rml | 2 +- addons/auction/report/bids_phones_details.rml | 2 +- addons/auction/report/buyer_form_report.rml | 2 +- addons/auction/report/buyer_list.rml | 2 +- addons/auction/report/catelogwithpictures.rml | 2 +- addons/auction/report/deposit_seller.rml | 2 +- addons/auction/report/lots_list.rml | 2 +- addons/auction/report/lots_list_inventory.rml | 2 +- addons/auction/report/lots_list_landscape.rml | 2 +- addons/auction/report/seller_form_report.rml | 2 +- addons/base_module_doc_rst/report/ir_module_reference_graph.rml | 2 +- addons/delivery/report/shipping.rml | 2 +- addons/hr_attendance/report/attendance_errors.rml | 2 +- addons/hr_expense/report/expense.rml | 2 +- addons/hr_payroll/report/report_contribution_register.rml | 2 +- addons/hr_payroll/report/report_payslip.rml | 2 +- addons/hr_payroll/report/report_payslip_details.rml | 2 +- addons/hr_timesheet_invoice/report/account_analytic_profit.rml | 2 +- addons/hr_timesheet_invoice/report/cost_ledger.rml | 2 +- addons/l10n_fr/report/bilan_report.rml | 2 +- addons/l10n_fr/report/compute_resultant_report.rml | 2 +- addons/lunch/report/order.rml | 2 +- addons/marketing_campaign_crm_demo/crm_lead_mc_demo_report.rml | 2 +- addons/mrp/report/bom_structure.rml | 2 +- addons/mrp/report/order.rml | 2 +- addons/mrp_repair/report/order.rml | 2 +- addons/point_of_sale/report/account_statement.rml | 2 +- addons/point_of_sale/report/all_closed_cashbox_of_the_day.rml | 2 +- addons/point_of_sale/report/pos_details.rml | 2 +- addons/point_of_sale/report/pos_details_summary.rml | 2 +- addons/point_of_sale/report/pos_lines.rml | 2 +- addons/point_of_sale/report/pos_payment_report.rml | 2 +- addons/point_of_sale/report/pos_payment_report_user.rml | 2 +- addons/point_of_sale/report/pos_sales_user.rml | 2 +- addons/point_of_sale/report/pos_sales_user_today.rml | 2 +- addons/point_of_sale/report/pos_users_product.rml | 2 +- addons/product/report/product_pricelist.rml | 2 +- addons/project_planning/report/report_planning.rml | 2 +- addons/purchase/report/order.rml | 2 +- addons/purchase/report/request_quotation.rml | 2 +- addons/purchase_requisition/report/purchase_requisition.rml | 2 +- addons/report_intrastat/report/invoice.rml | 2 +- addons/sale/report/sale_order.rml | 2 +- addons/sale_layout/report/report_sale_layout.rml | 2 +- addons/stock/report/lot_overview.rml | 2 +- addons/stock/report/lot_overview_all.rml | 2 +- addons/stock/report/picking.rml | 2 +- addons/stock/report/stock_inventory_move.rml | 2 +- 83 files changed, 83 insertions(+), 83 deletions(-) diff --git a/addons/account/project/report/analytic_balance.rml b/addons/account/project/report/analytic_balance.rml index 77182b5bb9f..0c488eac400 100644 --- a/addons/account/project/report/analytic_balance.rml +++ b/addons/account/project/report/analytic_balance.rml @@ -1,6 +1,6 @@ -