From 34d45085358552794ee6303b387424ef1d409106 Mon Sep 17 00:00:00 2001 From: Christophe Combelles Date: Sun, 8 Feb 2015 23:30:40 +0200 Subject: [PATCH 01/39] [FIX] hr_payroll: nicer error message for some bad rule definitions For example avoids a crash with: TypeError: can't multiply sequence by non-int of type 'float' when setting the quantity field of a salary rule to `1,0` instead of `1.0`. Fixes #5154 Closes #5155 --- addons/hr_payroll/hr_payroll.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/addons/hr_payroll/hr_payroll.py b/addons/hr_payroll/hr_payroll.py index 90cf53775c5..52e2a6f30c8 100644 --- a/addons/hr_payroll/hr_payroll.py +++ b/addons/hr_payroll/hr_payroll.py @@ -868,18 +868,20 @@ result = rules.NET > categories.NET * 0.10''', rule = self.browse(cr, uid, rule_id, context=context) if rule.amount_select == 'fix': try: - return rule.amount_fix, eval(rule.quantity, localdict), 100.0 + return rule.amount_fix, float(eval(rule.quantity, localdict)), 100.0 except: raise osv.except_osv(_('Error!'), _('Wrong quantity defined for salary rule %s (%s).')% (rule.name, rule.code)) elif rule.amount_select == 'percentage': try: - return eval(rule.amount_percentage_base, localdict), eval(rule.quantity, localdict), rule.amount_percentage + return (float(eval(rule.amount_percentage_base, localdict)), + float(eval(rule.quantity, localdict)), + rule.amount_percentage) except: raise osv.except_osv(_('Error!'), _('Wrong percentage base or quantity defined for salary rule %s (%s).')% (rule.name, rule.code)) else: try: eval(rule.amount_python_compute, localdict, mode='exec', nocopy=True) - return localdict['result'], 'result_qty' in localdict and localdict['result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0 + return float(localdict['result']), 'result_qty' in localdict and localdict['result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0 except: raise osv.except_osv(_('Error!'), _('Wrong python code defined for salary rule %s (%s).')% (rule.name, rule.code)) From 0e248245dd82f8267ef8820e19f6050fa94d5fd6 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Thu, 12 Feb 2015 14:09:49 +0100 Subject: [PATCH 02/39] [FIX] website_quote: prevent to add options to a confirmed order --- addons/website_quote/controllers/main.py | 2 ++ addons/website_quote/views/website_quotation.xml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/website_quote/controllers/main.py b/addons/website_quote/controllers/main.py index 8fab07fcce2..a6de9543e47 100644 --- a/addons/website_quote/controllers/main.py +++ b/addons/website_quote/controllers/main.py @@ -140,6 +140,8 @@ class sale_quote(http.Controller): order = request.registry.get('sale.order').browse(request.cr, SUPERUSER_ID, order_id) if token != order.access_token: return request.website.render('website.404') + if order.state not in ['draft', 'sent']: + return request.website.render('website.http_error', {'status_code': 'Forbidden', 'status_message': _('You cannot add options to a confirmed order.')}) option_obj = request.registry.get('sale.order.option') option = option_obj.browse(request.cr, SUPERUSER_ID, option_id) diff --git a/addons/website_quote/views/website_quotation.xml b/addons/website_quote/views/website_quotation.xml index 95e2af93811..16c39e0bf52 100644 --- a/addons/website_quote/views/website_quotation.xml +++ b/addons/website_quote/views/website_quotation.xml @@ -458,7 +458,7 @@ - + From a67747f77e43f9708728586852d72564fe298320 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Thu, 12 Feb 2015 14:11:49 +0100 Subject: [PATCH 03/39] [FIX] website_quote: duplicate sales order options on order duplication --- addons/website_quote/models/order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website_quote/models/order.py b/addons/website_quote/models/order.py index 85ffc2b69f5..169de81f15a 100644 --- a/addons/website_quote/models/order.py +++ b/addons/website_quote/models/order.py @@ -140,7 +140,7 @@ class sale_order(osv.osv): 'template_id': fields.many2one('sale.quote.template', 'Quote Template', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}), 'website_description': fields.html('Description'), - 'options' : fields.one2many('sale.order.option', 'order_id', 'Optional Products Lines'), + 'options' : fields.one2many('sale.order.option', 'order_id', 'Optional Products Lines', copy=True), 'validity_date': fields.date('Expiry Date'), 'amount_undiscounted': fields.function(_get_total, string='Amount Before Discount', type="float", digits_compute=dp.get_precision('Account')) From c9154e08aab9114ce1e269f91c7c6d3972d4b114 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Thu, 12 Feb 2015 14:57:31 +0100 Subject: [PATCH 04/39] [FIX] api: environment recomputation In a workflow context (for instance, in the invoice workflow), context is not passed. Therefore, relying on the 'recompute' key being the context in order to not recompute the fields does not work with Workflows. It leads to huge performance issues, as fields are recomputed recursively (instead of sequentially) when several records are implied. For instance, when reconciling several invoices with one payment (100 invoices with 1 payment for instance), records of each invoice are recomputed uselessly in each workflow call (for each "confirm_paid" method done for each invoice). With a significant number of invoices (100, for instance), it even leads to a "Maximum recursion depth reached" errror. closes #4905 --- addons/account/account_move_line.py | 3 +- openerp/api.py | 14 +++++ openerp/models.py | 11 ++-- openerp/osv/fields.py | 95 +++++++++++++++-------------- 4 files changed, 70 insertions(+), 53 deletions(-) diff --git a/addons/account/account_move_line.py b/addons/account/account_move_line.py index b852672ad8f..75f6198c7d2 100644 --- a/addons/account/account_move_line.py +++ b/addons/account/account_move_line.py @@ -1391,7 +1391,8 @@ class account_move_line(osv.osv): self.create(cr, uid, data, context) del vals['account_tax_id'] - if check and not context.get('novalidate') and (context.get('recompute', True) or journal.entry_posted): + recompute = journal.env.recompute and context.get('recompute', True) + if check and not context.get('novalidate') and (recompute or journal.entry_posted): tmp = move_obj.validate(cr, uid, [vals['move_id']], context) if journal.entry_posted and tmp: move_obj.button_validate(cr,uid, [vals['move_id']], context) diff --git a/openerp/api.py b/openerp/api.py index 070dfd4e69b..4ab512be6a9 100644 --- a/openerp/api.py +++ b/openerp/api.py @@ -890,6 +890,19 @@ class Environment(object): if invalids: raise Warning('Invalid cache for fields\n' + pformat(invalids)) + @property + def recompute(self): + return self.all.recompute + + @contextmanager + def norecompute(self): + tmp = self.all.recompute + self.all.recompute = False + try: + yield + finally: + self.all.recompute = tmp + class Environments(object): """ A common object for all environments in a request. """ @@ -897,6 +910,7 @@ class Environments(object): self.envs = WeakSet() # weak set of environments self.todo = {} # recomputations {field: [records]} self.mode = False # flag for draft/onchange + self.recompute = True def add(self, env): """ Add the environment `env`. """ diff --git a/openerp/models.py b/openerp/models.py index fde2e393751..a9f32c72588 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -3973,7 +3973,7 @@ class BaseModel(object): self.pool[model_name]._store_set_values(cr, user, todo, fields_to_recompute, context) # recompute new-style fields - if context.get('recompute', True): + if recs.env.recompute and context.get('recompute', True): recs.recompute() self.step_workflow(cr, user, ids, context=context) @@ -4220,7 +4220,7 @@ class BaseModel(object): # check Python constraints recs._validate_fields(vals) - if context.get('recompute', True): + if recs.env.recompute and context.get('recompute', True): result += self._store_get_values(cr, user, [id_new], list(set(vals.keys() + self._inherits.values())), context) @@ -4233,7 +4233,7 @@ class BaseModel(object): # recompute new-style fields recs.recompute() - if self._log_create and context.get('recompute', True): + if self._log_create and recs.env.recompute and context.get('recompute', True): message = self._description + \ " '" + \ self.name_get(cr, user, [id_new], context=context)[0][1] + \ @@ -5629,12 +5629,13 @@ class BaseModel(object): field, recs = self.env.get_todo() # evaluate the fields to recompute, and save them to database names = [f.name for f in field.computed_fields if f.store] - for rec, rec1 in zip(recs, recs.with_context(recompute=False)): + for rec in recs: try: values = rec._convert_to_write({ name: rec[name] for name in names }) - rec1._write(values) + with rec.env.norecompute(): + rec._write(values) except MissingError: pass # mark the computed fields as done diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 25028e16305..d0fc0dfcaea 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -736,57 +736,58 @@ class one2many(_column): result = [] context = dict(context or {}) context.update(self._context) - context['recompute'] = False # recomputation is done by outer create/write if not values: return obj = obj.pool[self._obj] - _table = obj._table - for act in values: - if act[0] == 0: - act[2][self._fields_id] = id - id_new = obj.create(cr, user, act[2], context=context) - result += obj._store_get_values(cr, user, [id_new], act[2].keys(), context) - elif act[0] == 1: - obj.write(cr, user, [act[1]], act[2], context=context) - elif act[0] == 2: - obj.unlink(cr, user, [act[1]], context=context) - elif act[0] == 3: - inverse_field = obj._fields.get(self._fields_id) - assert inverse_field, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o' - # if the model has on delete cascade, just delete the row - if inverse_field.ondelete == "cascade": + rec = obj.browse(cr, user, [], context=context) + with rec.env.norecompute(): + _table = obj._table + for act in values: + if act[0] == 0: + act[2][self._fields_id] = id + id_new = obj.create(cr, user, act[2], context=context) + result += obj._store_get_values(cr, user, [id_new], act[2].keys(), context) + elif act[0] == 1: + obj.write(cr, user, [act[1]], act[2], context=context) + elif act[0] == 2: obj.unlink(cr, user, [act[1]], context=context) - else: - cr.execute('update '+_table+' set '+self._fields_id+'=null where id=%s', (act[1],)) - elif act[0] == 4: - # table of the field (parent_model in case of inherit) - field = obj.pool[self._obj]._fields[self._fields_id] - field_model = field.base_field.model_name - field_table = obj.pool[field_model]._table - cr.execute("select 1 from {0} where id=%s and {1}=%s".format(field_table, self._fields_id), (act[1], id)) - if not cr.fetchone(): - # Must use write() to recompute parent_store structure if needed and check access rules - obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {}) - elif act[0] == 5: - inverse_field = obj._fields.get(self._fields_id) - assert inverse_field, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o' - # if the o2m has a static domain we must respect it when unlinking - domain = self._domain(obj) if callable(self._domain) else self._domain - extra_domain = domain or [] - ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context) - # If the model has cascade deletion, we delete the rows because it is the intended behavior, - # otherwise we only nullify the reverse foreign key column. - if inverse_field.ondelete == "cascade": - obj.unlink(cr, user, ids_to_unlink, context=context) - else: - obj.write(cr, user, ids_to_unlink, {self._fields_id: False}, context=context) - elif act[0] == 6: - # Must use write() to recompute parent_store structure if needed - obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {}) - ids2 = act[2] or [0] - cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2)) - ids3 = map(lambda x:x[0], cr.fetchall()) - obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {}) + elif act[0] == 3: + inverse_field = obj._fields.get(self._fields_id) + assert inverse_field, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o' + # if the model has on delete cascade, just delete the row + if inverse_field.ondelete == "cascade": + obj.unlink(cr, user, [act[1]], context=context) + else: + cr.execute('update '+_table+' set '+self._fields_id+'=null where id=%s', (act[1],)) + elif act[0] == 4: + # table of the field (parent_model in case of inherit) + field = obj.pool[self._obj]._fields[self._fields_id] + field_model = field.base_field.model_name + field_table = obj.pool[field_model]._table + cr.execute("select 1 from {0} where id=%s and {1}=%s".format(field_table, self._fields_id), (act[1], id)) + if not cr.fetchone(): + # Must use write() to recompute parent_store structure if needed and check access rules + obj.write(cr, user, [act[1]], {self._fields_id:id}, context=context or {}) + elif act[0] == 5: + inverse_field = obj._fields.get(self._fields_id) + assert inverse_field, 'Trying to unlink the content of a o2m but the pointed model does not have a m2o' + # if the o2m has a static domain we must respect it when unlinking + domain = self._domain(obj) if callable(self._domain) else self._domain + extra_domain = domain or [] + ids_to_unlink = obj.search(cr, user, [(self._fields_id,'=',id)] + extra_domain, context=context) + # If the model has cascade deletion, we delete the rows because it is the intended behavior, + # otherwise we only nullify the reverse foreign key column. + if inverse_field.ondelete == "cascade": + obj.unlink(cr, user, ids_to_unlink, context=context) + else: + obj.write(cr, user, ids_to_unlink, {self._fields_id: False}, context=context) + elif act[0] == 6: + # Must use write() to recompute parent_store structure if needed + obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {}) + ids2 = act[2] or [0] + cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2)) + ids3 = map(lambda x:x[0], cr.fetchall()) + obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {}) return result def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None): From f2ab162d31d6bcf850c94706d37e56bcdd966337 Mon Sep 17 00:00:00 2001 From: moylop260 Date: Wed, 11 Feb 2015 14:31:31 -0600 Subject: [PATCH 05/39] [FIX] l10n_mx: Set reconcile=True on receivable and payable accounts This is necessary to enable reconciliations on these accounts, and also ensures the residual amount on invoices will be properly computed. Closes #5235 --- addons/l10n_mx/data/account_chart.xml | 36 +++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/addons/l10n_mx/data/account_chart.xml b/addons/l10n_mx/data/account_chart.xml index c28d8da8a44..5090c831839 100644 --- a/addons/l10n_mx/data/account_chart.xml +++ b/addons/l10n_mx/data/account_chart.xml @@ -1332,7 +1332,7 @@ Cuentas del plan 1121001000 - + receivable @@ -1341,7 +1341,7 @@ Cuentas del plan 1122001000 - + receivable @@ -1359,7 +1359,7 @@ Cuentas del plan 1122004000 - + receivable @@ -1368,7 +1368,7 @@ Cuentas del plan 1122005000 - + receivable @@ -1395,7 +1395,7 @@ Cuentas del plan 1125001000 - + receivable @@ -1413,7 +1413,7 @@ Cuentas del plan 1125003000 - + receivable @@ -1467,7 +1467,7 @@ Cuentas del plan 1128001000 - + receivable @@ -1530,7 +1530,7 @@ Cuentas del plan 1129009000 - + receivable @@ -1881,7 +1881,7 @@ Cuentas del plan 1271005000 - + receivable @@ -1944,7 +1944,7 @@ Cuentas del plan 2122001000 - + payable @@ -1971,7 +1971,7 @@ Cuentas del plan 2122005000 - + payable @@ -1980,7 +1980,7 @@ Cuentas del plan 2124001000 - + payable @@ -1989,7 +1989,7 @@ Cuentas del plan 2151001000 - + payable @@ -1998,7 +1998,7 @@ Cuentas del plan 2151002000 - + receivable @@ -2151,7 +2151,7 @@ Cuentas del plan 2175008000 - + payable @@ -2205,7 +2205,7 @@ Cuentas del plan 2175014000 - + payable @@ -2223,7 +2223,7 @@ Cuentas del plan 2175017000 - + payable @@ -2232,7 +2232,7 @@ Cuentas del plan 2175018000 - + payable From c62be6167a79e1d581aea2d2ad9fd53fbb70b43d Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Thu, 12 Feb 2015 16:37:37 +0100 Subject: [PATCH 06/39] [FIX] website_sale: price overlapping product name In ecommerce products list view, if the product name was shorter than the product price, than, the product price overlapped the product description Besides, this display: inline-block; has no useful utility (at least not known), and it should be set in a CSS property anyway --- addons/website_sale/views/templates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index bb9cd2a4ca1..45ed4ff71bf 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -70,7 +70,7 @@