From 4d8337fec87332669bcdb377a427da50a408b20d Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Mon, 4 Aug 2014 14:22:54 +0200 Subject: [PATCH 01/26] [FIX] mrp: better selection of bom When searching for a bom based on a product.product, the method used to return any bom linked to the template (so searching for bom for variant A could return bom for variant B if both share the same product.template). This will allow to return bom not linked to a variant without returning the one of another variant (opw 609358) --- addons/mrp/mrp.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/addons/mrp/mrp.py b/addons/mrp/mrp.py index ca4109c1b9a..ba49ba7a9b4 100644 --- a/addons/mrp/mrp.py +++ b/addons/mrp/mrp.py @@ -240,16 +240,27 @@ class mrp_bom(osv.osv): """ if properties is None: properties = [] - domain = None if product_id: - domain = ['|',('product_id', '=', product_id),('product_tmpl_id.product_variant_ids', '=', product_id)] - else: + if not product_tmpl_id: + product_tmpl_id = self.pool['product.product'].browse(cr, uid, product_id).product_tmpl_id.id + domain = [ + '|', + ('product_id', '=', product_id), + '&', + ('product_id', '=', False), + ('product_tmpl_id', '=', product_tmpl_id) + ] + elif product_tmpl_id: domain = [('product_id', '=', False), ('product_tmpl_id', '=', product_tmpl_id)] + else: + # neither product nor template, makes no sense to search + return False if product_uom: domain += [('product_uom','=',product_uom)] domain = domain + [ '|', ('date_start', '=', False), ('date_start', '<=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)), '|', ('date_stop', '=', False), ('date_stop', '>=', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))] - ids = self.search(cr, uid, domain) + # order to prioritize bom with product_id over the one without + ids = self.search(cr, uid, domain, order='product_id') for bom in self.pool.get('mrp.bom').browse(cr, uid, ids): if not set(map(int,bom.property_ids or [])) - set(properties or []): return bom.id From 56c54a52dfc9ef6a39a37b24afd7ae30597a5c0f Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Mon, 4 Aug 2014 11:37:16 +0200 Subject: [PATCH 02/26] [IMP] base/res_users: improve reading/writing of reified group fields - simplify the code by removing unused cases - do not modify input argument 'value' of create/write - do not call BaseModel.read() with reified group fields (this causes warnings) --- openerp/addons/base/res/res_users.py | 112 ++++++++++++++------------- 1 file changed, 59 insertions(+), 53 deletions(-) diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index f1ddf4f1b42..f2bc917d09e 100644 --- a/openerp/addons/base/res/res_users.py +++ b/openerp/addons/base/res/res_users.py @@ -22,6 +22,7 @@ import itertools import logging from functools import partial +from itertools import repeat from lxml import etree from lxml.builder import E @@ -650,25 +651,30 @@ class users_implied(osv.osv): # Naming conventions for reified groups fields: # - boolean field 'in_group_ID' is True iff # ID is in 'groups_id' -# - boolean field 'in_groups_ID1_..._IDk' is True iff -# any of ID1, ..., IDk is in 'groups_id' # - selection field 'sel_groups_ID1_..._IDk' is ID iff # ID is in 'groups_id' and ID is maximal in the set {ID1, ..., IDk} #---------------------------------------------------------- -def name_boolean_group(id): return 'in_group_' + str(id) -def name_boolean_groups(ids): return 'in_groups_' + '_'.join(map(str, ids)) -def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids)) +def name_boolean_group(id): + return 'in_group_' + str(id) + +def name_selection_groups(ids): + return 'sel_groups_' + '_'.join(map(str, ids)) + +def is_boolean_group(name): + return name.startswith('in_group_') + +def is_selection_groups(name): + return name.startswith('sel_groups_') -def is_boolean_group(name): return name.startswith('in_group_') -def is_boolean_groups(name): return name.startswith('in_groups_') -def is_selection_groups(name): return name.startswith('sel_groups_') def is_reified_group(name): - return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name) + return is_boolean_group(name) or is_selection_groups(name) -def get_boolean_group(name): return int(name[9:]) -def get_boolean_groups(name): return map(int, name[10:].split('_')) -def get_selection_groups(name): return map(int, name[11:].split('_')) +def get_boolean_group(name): + return int(name[9:]) + +def get_selection_groups(name): + return map(int, name[11:].split('_')) def partition(f, xs): "return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))" @@ -789,45 +795,39 @@ class users_view(osv.osv): _inherit = 'res.users' def create(self, cr, uid, values, context=None): - self._set_reified_groups(values) + values = self._remove_reified_groups(values) return super(users_view, self).create(cr, uid, values, context) def write(self, cr, uid, ids, values, context=None): - self._set_reified_groups(values) + values = self._remove_reified_groups(values) return super(users_view, self).write(cr, uid, ids, values, context) - def _set_reified_groups(self, values): - """ reflect reified group fields in values['groups_id'] """ - if 'groups_id' in values: - # groups are already given, ignore group fields - for f in filter(is_reified_group, values.iterkeys()): - del values[f] - return + def _remove_reified_groups(self, values): + """ return `values` without reified group fields """ + add, rem = [], [] + values1 = {} - add, remove = [], [] - for f in values.keys(): - if is_boolean_group(f): - target = add if values.pop(f) else remove - target.append(get_boolean_group(f)) - elif is_boolean_groups(f): - if not values.pop(f): - remove.extend(get_boolean_groups(f)) - elif is_selection_groups(f): - remove.extend(get_selection_groups(f)) - selected = values.pop(f) - if selected: - add.append(selected) - # update values *only* if groups are being modified, otherwise - # we introduce spurious changes that might break the super.write() call. - if add or remove: - # remove groups in 'remove' and add groups in 'add' - values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add] + for key, val in values.iteritems(): + if is_boolean_group(key): + (add if val else rem).append(get_boolean_group(key)) + elif is_selection_groups(key): + rem += get_selection_groups(key) + if val: + add.append(val) + else: + values1[key] = val + + if 'groups_id' not in values and (add or rem): + # remove group ids in `rem` and add group ids in `add` + values1['groups_id'] = zip(repeat(3), rem) + zip(repeat(4), add) + + return values1 def default_get(self, cr, uid, fields, context=None): group_fields, fields = partition(is_reified_group, fields) fields1 = (fields + ['groups_id']) if group_fields else fields values = super(users_view, self).default_get(cr, uid, fields1, context) - self._get_reified_groups(group_fields, values) + self._add_reified_groups(group_fields, values) # add "default_groups_ref" inside the context to set default value for group_id with xml values if 'groups_id' in fields and isinstance(context.get("default_groups_ref"), list): @@ -846,29 +846,35 @@ class users_view(osv.osv): return values def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'): - fields_get = fields if fields is not None else self.fields_get(cr, uid, context=context).keys() - group_fields, _ = partition(is_reified_group, fields_get) + # determine whether reified groups fields are required, and which ones + fields1 = fields or self.fields_get(cr, uid, context=context).keys() + group_fields, other_fields = partition(is_reified_group, fields1) - inject_groups_id = group_fields and fields and 'groups_id' not in fields - if inject_groups_id: - fields.append('groups_id') - res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load) + # read regular fields (other_fields); add 'groups_id' if necessary + drop_groups_id = False + if group_fields and fields: + if 'groups_id' not in other_fields: + other_fields.append('groups_id') + drop_groups_id = True + else: + other_fields = fields - if res and group_fields: + res = super(users_view, self).read(cr, uid, ids, other_fields, context=context, load=load) + + # post-process result to add reified group fields + if group_fields: for values in (res if isinstance(res, list) else [res]): - self._get_reified_groups(group_fields, values) - if inject_groups_id: + self._add_reified_groups(group_fields, values) + if drop_groups_id: values.pop('groups_id', None) return res - def _get_reified_groups(self, fields, values): - """ compute the given reified group fields from values['groups_id'] """ + def _add_reified_groups(self, fields, values): + """ add the given reified group fields into `values` """ gids = set(parse_m2m(values.get('groups_id') or [])) for f in fields: if is_boolean_group(f): values[f] = get_boolean_group(f) in gids - elif is_boolean_groups(f): - values[f] = not gids.isdisjoint(get_boolean_groups(f)) elif is_selection_groups(f): selected = [gid for gid in get_selection_groups(f) if gid in gids] values[f] = selected and selected[-1] or False From 5f6fc4735a9a09c350dbbb789f3d39000ca3601c Mon Sep 17 00:00:00 2001 From: Samus CTO Date: Tue, 5 Aug 2014 11:34:19 +0200 Subject: [PATCH 03/26] [IMP] Do not create FK when destination model is _auto=False It is not useful to try to create foreign keys when the destination model is a PostgreSQL view for example. We already do this kind of verifications but ir.actions and transient models but did not for _auto. --- openerp/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/models.py b/openerp/models.py index af55db1f1cb..c28a231d116 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -2621,7 +2621,7 @@ class BaseModel(object): if isinstance(f, fields.many2one) or (isinstance(f, fields.function) and f._type == 'many2one' and f.store): dest_model = self.pool[f._obj] - if dest_model._table != 'ir_actions': + if dest_model._auto and dest_model._table != 'ir_actions': self._m2o_fix_foreign_key(cr, self._table, k, dest_model, f.ondelete) # The field doesn't exist in database. Create it if necessary. From 2d54afef9e72a27e9c8e60a9cecb41f08a0cb693 Mon Sep 17 00:00:00 2001 From: cod-odoo Date: Mon, 28 Jul 2014 16:10:46 +0530 Subject: [PATCH 04/26] [FIX] account: prevent crash onchange company_id (rebase of #1534) --- addons/account/account_invoice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index 1092ec33aea..5b45f8dd02a 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -572,8 +572,8 @@ class account_invoice(models.Model): if 'journal_id' in journal_defaults: values['journal_id'] = journal_defaults['journal_id'] if not values.get('journal_id'): - field_desc = journals.fields_get(['journal_id']) - type_label = next(t for t, label in field_desc['journal_id']['selection'] if t == journal_type) + field_desc = journals.fields_get(['type']) + type_label = next(t for t, label in field_desc['type']['selection'] if t == journal_type) action = self.env.ref('account.action_account_journal_form') msg = _('Cannot find any account journal of type "%s" for this company, You should create one.\n Please go to Journal Configuration') % type_label raise RedirectWarning(msg, action.id, _('Go to the configuration panel')) From 54b901effdd2ebbf5fbde33cf0b23e52c135821b Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Tue, 5 Aug 2014 14:50:44 +0200 Subject: [PATCH 05/26] [IMP] models: turn _patch_method() and _revert_method() into class methods This makes the patching mechanism more flexible, and enables patching BaseModel, for instance. This should fix #1501. --- openerp/models.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openerp/models.py b/openerp/models.py index c28a231d116..7d328dea39b 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -4973,9 +4973,10 @@ class BaseModel(object): """ stuff to do right after the registry is built """ pass - def _patch_method(self, name, method): + @classmethod + def _patch_method(cls, name, method): """ Monkey-patch a method for all instances of this model. This replaces - the method called `name` by `method` in `self`'s class. + the method called `name` by `method` in the given class. The original method is then accessible via ``method.origin``, and it can be restored with :meth:`~._revert_method`. @@ -4996,7 +4997,6 @@ class BaseModel(object): # restore the original method model._revert_method('write') """ - cls = type(self) origin = getattr(cls, name) method.origin = origin # propagate decorators from origin to method, and apply api decorator @@ -5004,11 +5004,11 @@ class BaseModel(object): wrapped.origin = origin setattr(cls, name, wrapped) - def _revert_method(self, name): - """ Revert the original method of `self` called `name`. + @classmethod + def _revert_method(cls, name): + """ Revert the original method called `name` in the given class. See :meth:`~._patch_method`. """ - cls = type(self) method = getattr(cls, name) setattr(cls, name, method.origin) From 497883cc51c5a563a31dd892c536d8799933def5 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Tue, 5 Aug 2014 15:44:57 +0200 Subject: [PATCH 06/26] [FIX] web_kanban: parent can be undefined in some cases opw-611106, when clicking quickly Sales > Opportunities than Sales > Leads. --- addons/web_kanban/static/src/xml/web_kanban.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/web_kanban/static/src/xml/web_kanban.xml b/addons/web_kanban/static/src/xml/web_kanban.xml index c1d504e51be..aebdbacb9dd 100644 --- a/addons/web_kanban/static/src/xml/web_kanban.xml +++ b/addons/web_kanban/static/src/xml/web_kanban.xml @@ -45,8 +45,8 @@ From 7d7f6fefb19a43f7bf3d2e31da5d94df7f7fa857 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Tue, 5 Aug 2014 16:01:55 +0200 Subject: [PATCH 07/26] [FIX] purchase: now allow 'can be purchased' to be checked even if not variant Before, the field purchase_ok was set as readonly on the product form if the product wasn't a variant, to avoid users to set all the products variants associated to the product template to be set as purchase able in one action. We now consider that this can be done by the end user --- addons/purchase/purchase_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/purchase/purchase_view.xml b/addons/purchase/purchase_view.xml index 2f88439f076..570583dcb48 100644 --- a/addons/purchase/purchase_view.xml +++ b/addons/purchase/purchase_view.xml @@ -594,7 +594,7 @@
- +
From 836b0f864eb0603af12f762f207252e8bdec18c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 5 Aug 2014 14:49:52 +0200 Subject: [PATCH 08/26] [FIX] hr_contract: count the number of contracts of an employee as superuser_id as this field is required in the form view. However no data is displayed, and no data about the contracts are leaked, so no security holes. --- addons/hr_contract/hr_contract.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/addons/hr_contract/hr_contract.py b/addons/hr_contract/hr_contract.py index 8b58c3efd61..b80b93bc3a5 100644 --- a/addons/hr_contract/hr_contract.py +++ b/addons/hr_contract/hr_contract.py @@ -20,6 +20,7 @@ ############################################################################## import time +from openerp import SUPERUSER_ID from openerp.osv import fields, osv class hr_employee(osv.osv): @@ -41,7 +42,7 @@ class hr_employee(osv.osv): def _contracts_count(self, cr, uid, ids, field_name, arg, context=None): Contract = self.pool['hr.contract'] return { - employee_id: Contract.search_count(cr,uid, [('employee_id', '=', employee_id)], context=context) + employee_id: Contract.search_count(cr, SUPERUSER_ID, [('employee_id', '=', employee_id)], context=context) for employee_id in ids } @@ -53,7 +54,7 @@ class hr_employee(osv.osv): 'vehicle': fields.char('Company Vehicle'), 'vehicle_distance': fields.integer('Home-Work Dist.', help="In kilometers"), 'contract_ids': fields.one2many('hr.contract', 'employee_id', 'Contracts'), - 'contract_id':fields.function(_get_latest_contract, string='Contract', type='many2one', relation="hr.contract", help='Latest contract of the employee'), + 'contract_id': fields.function(_get_latest_contract, string='Contract', type='many2one', relation="hr.contract", help='Latest contract of the employee'), 'contracts_count': fields.function(_contracts_count, type='integer', string='Contracts'), } From 4c18c5fb6e518e9b36f6f9bb9c279e530ecde255 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Wed, 9 Jul 2014 17:06:29 +0200 Subject: [PATCH 09/26] [FIX] fields: convert_to_cache() on *2many fields must take record's current value The existing code was buggy when writing on *2many fields with a list of commands: the value was converted for the cache, but taking an empty recordset as the current value of the field. --- openerp/fields.py | 71 +++++++++++++++++++++++++---------------------- openerp/models.py | 17 ++++++++---- 2 files changed, 50 insertions(+), 38 deletions(-) diff --git a/openerp/fields.py b/openerp/fields.py index d85ce82eccb..e9fd90805f2 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -609,11 +609,13 @@ class Field(object): """ return the null value for this field in the given environment """ return False - def convert_to_cache(self, value, env, validate=True): + def convert_to_cache(self, value, record, validate=True): """ convert `value` to the cache level in `env`; `value` may come from an assignment, or have the format of methods :meth:`BaseModel.read` or :meth:`BaseModel.write` + :param record: the target record for the assignment, or an empty recordset + :param bool validate: when True, field-specific validation of `value` will be performed """ @@ -698,7 +700,7 @@ class Field(object): record.ensure_one() # adapt value to the cache level - value = self.convert_to_cache(value, env) + value = self.convert_to_cache(value, record) if env.in_draft or not record.id: # determine dependent fields @@ -861,7 +863,7 @@ class Boolean(Field): """ Boolean field. """ type = 'boolean' - def convert_to_cache(self, value, env, validate=True): + def convert_to_cache(self, value, record, validate=True): return bool(value) def convert_to_export(self, value, env): @@ -874,7 +876,7 @@ class Integer(Field): """ Integer field. """ type = 'integer' - def convert_to_cache(self, value, env, validate=True): + def convert_to_cache(self, value, record, validate=True): return int(value or 0) def convert_to_read(self, value, use_name_get=True): @@ -914,7 +916,7 @@ class Float(Field): _column_digits = property(lambda self: not callable(self._digits) and self._digits) _column_digits_compute = property(lambda self: callable(self._digits) and self._digits) - def convert_to_cache(self, value, env, validate=True): + def convert_to_cache(self, value, record, validate=True): # apply rounding here, otherwise value in cache may be wrong! if self.digits: return float_round(float(value or 0.0), precision_digits=self.digits[1]) @@ -948,7 +950,7 @@ class Char(_String): _related_size = property(attrgetter('size')) _description_size = property(attrgetter('size')) - def convert_to_cache(self, value, env, validate=True): + def convert_to_cache(self, value, record, validate=True): return bool(value) and ustr(value)[:self.size] @@ -962,7 +964,7 @@ class Text(_String): """ type = 'text' - def convert_to_cache(self, value, env, validate=True): + def convert_to_cache(self, value, record, validate=True): return bool(value) and ustr(value) @@ -970,7 +972,7 @@ class Html(_String): """ Html field. """ type = 'html' - def convert_to_cache(self, value, env, validate=True): + def convert_to_cache(self, value, record, validate=True): return bool(value) and html_sanitize(value) @@ -1019,7 +1021,7 @@ class Date(Field): """ Convert a :class:`date` value into the format expected by the ORM. """ return value.strftime(DATE_FORMAT) - def convert_to_cache(self, value, env, validate=True): + def convert_to_cache(self, value, record, validate=True): if not value: return False if isinstance(value, basestring): @@ -1084,7 +1086,7 @@ class Datetime(Field): """ Convert a :class:`datetime` value into the format expected by the ORM. """ return value.strftime(DATETIME_FORMAT) - def convert_to_cache(self, value, env, validate=True): + def convert_to_cache(self, value, record, validate=True): if not value: return False if isinstance(value, basestring): @@ -1164,10 +1166,10 @@ class Selection(Field): selection = selection(env[self.model_name]) return [value for value, _ in selection] - def convert_to_cache(self, value, env, validate=True): + def convert_to_cache(self, value, record, validate=True): if not validate: return value or False - if value in self.get_values(env): + if value in self.get_values(record.env): return value elif not value: return False @@ -1204,14 +1206,14 @@ class Reference(Selection): _column_size = property(attrgetter('size')) - def convert_to_cache(self, value, env, validate=True): + def convert_to_cache(self, value, record, validate=True): if isinstance(value, BaseModel): - if ((not validate or value._name in self.get_values(env)) + if ((not validate or value._name in self.get_values(record.env)) and len(value) <= 1): - return value.with_env(env) or False + return value.with_env(record.env) or False elif isinstance(value, basestring): res_model, res_id = value.split(',') - return env[res_model].browse(int(res_id)) + return record.env[res_model].browse(int(res_id)) elif not value: return False raise ValueError("Wrong value for %s: %r" % (self, value)) @@ -1302,19 +1304,19 @@ class Many2one(_Relational): """ Update the cached value of `self` for `records` with `value`. """ records._cache[self] = value - def convert_to_cache(self, value, env, validate=True): + def convert_to_cache(self, value, record, validate=True): if isinstance(value, (NoneType, int)): - return env[self.comodel_name].browse(value) + return record.env[self.comodel_name].browse(value) if isinstance(value, BaseModel): if value._name == self.comodel_name and len(value) <= 1: - return value.with_env(env) + return value.with_env(record.env) raise ValueError("Wrong value for %s: %r" % (self, value)) elif isinstance(value, tuple): - return env[self.comodel_name].browse(value[0]) + return record.env[self.comodel_name].browse(value[0]) elif isinstance(value, dict): - return env[self.comodel_name].new(value) + return record.env[self.comodel_name].new(value) else: - return env[self.comodel_name].browse(value) + return record.env[self.comodel_name].browse(value) def convert_to_read(self, value, use_name_get=True): if use_name_get and value: @@ -1355,27 +1357,30 @@ class _RelationalMulti(_Relational): for record in records: record._cache[self] = record[self.name] | value - def convert_to_cache(self, value, env, validate=True): + def convert_to_cache(self, value, record, validate=True): if isinstance(value, BaseModel): if value._name == self.comodel_name: - return value.with_env(env) + return value.with_env(record.env) elif isinstance(value, list): # value is a list of record ids or commands - result = env[self.comodel_name] + if not record.id: + record = record.browse() # new record has no value + result = record[self.name] + # modify result with the commands; + # beware to not introduce duplicates in result for command in value: if isinstance(command, (tuple, list)): if command[0] == 0: result += result.new(command[2]) elif command[0] == 1: - record = result.browse(command[1]) - record.update(command[2]) - result += record + result.browse(command[1]).update(command[2]) elif command[0] == 2: - pass + # note: the record will be deleted by write() + result -= result.browse(command[1]) elif command[0] == 3: - pass + result -= result.browse(command[1]) elif command[0] == 4: - result += result.browse(command[1]) + result += result.browse(command[1]) - result elif command[0] == 5: result = result.browse() elif command[0] == 6: @@ -1383,10 +1388,10 @@ class _RelationalMulti(_Relational): elif isinstance(command, dict): result += result.new(command) else: - result += result.browse(command) + result += result.browse(command) - result return result elif not value: - return self.null(env) + return self.null(record.env) raise ValueError("Wrong value for %s: %s" % (self, value)) def convert_to_read(self, value, use_name_get=True): diff --git a/openerp/models.py b/openerp/models.py index 7d328dea39b..2f3b90a89b3 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -3627,7 +3627,8 @@ class BaseModel(object): # put the values of pure new-style fields into cache, and inverse them if new_vals: - self._cache.update(self._convert_to_cache(new_vals)) + for record in self: + record._cache.update(record._convert_to_cache(new_vals, update=True)) for key in new_vals: self._fields[key].determine_inverse(self) @@ -5098,11 +5099,17 @@ class BaseModel(object): context = dict(args[0] if args else self._context, **kwargs) return self.with_env(self.env(context=context)) - def _convert_to_cache(self, values, validate=True): - """ Convert the `values` dictionary into cached values. """ + def _convert_to_cache(self, values, update=False, validate=True): + """ Convert the `values` dictionary into cached values. + + :param update: whether the conversion is made for updating `self`; + this is necessary for interpreting the commands of *2many fields + :param validate: whether values must be checked + """ fields = self._fields + target = self if update else self.browse() return { - name: fields[name].convert_to_cache(value, self.env, validate=validate) + name: fields[name].convert_to_cache(value, target, validate=validate) for name, value in values.iteritems() if name in fields } @@ -5191,7 +5198,7 @@ class BaseModel(object): exist in the database. """ record = self.browse([NewId()]) - record._cache.update(self._convert_to_cache(values)) + record._cache.update(record._convert_to_cache(values, update=True)) if record.env.in_onchange: # The cache update does not set inverse fields, so do it manually. From 52a37be447999700f2306cb1e2337795254decd8 Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Wed, 6 Aug 2014 10:28:16 +0200 Subject: [PATCH 10/26] [FIX] project_issue: reviewer_id doesn(t exist (partial revert of 44a5268c00443cfb0664d7679d93772422a7403) --- addons/project_issue/report/project_issue_report_view.xml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/addons/project_issue/report/project_issue_report_view.xml b/addons/project_issue/report/project_issue_report_view.xml index 3382c6e3625..67473875134 100644 --- a/addons/project_issue/report/project_issue_report_view.xml +++ b/addons/project_issue/report/project_issue_report_view.xml @@ -21,13 +21,6 @@ {'group_by': ['project_id', 'user_id']} - - By Reviewer - project.issue.report - [] - - {'group_by': ['project_id', 'reviewer_id']} - project.issue.report.select From 6a83425ba9b8eac5c948e6265f30d57ff42e3fbf Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Wed, 6 Aug 2014 11:20:38 +0200 Subject: [PATCH 11/26] [FIX] base: fixes #1167, change password wizard (wrong default value) The one2many field 'user_ids' was initialized with an incorrect value for the inverse field 'wizard_id', using a res.users id. The latter was causing a MissingError exception. The fix is simply to not define explicitly the inverse field in the one2many. Also did a small cleanup of the code of the wizard. --- openerp/addons/base/res/res_users.py | 39 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index f2bc917d09e..7102c009264 100644 --- a/openerp/addons/base/res/res_users.py +++ b/openerp/addons/base/res/res_users.py @@ -921,29 +921,26 @@ class change_password_wizard(osv.TransientModel): 'user_ids': fields.one2many('change.password.user', 'wizard_id', string='Users'), } - def default_get(self, cr, uid, fields, context=None): - if context == None: + def _default_user_ids(self, cr, uid, context=None): + if context is None: context = {} - user_ids = context.get('active_ids', []) - wiz_id = context.get('active_id', None) - res = [] - users = self.pool.get('res.users').browse(cr, uid, user_ids, context=context) - for user in users: - res.append((0, 0, { - 'wizard_id': wiz_id, - 'user_id': user.id, - 'user_login': user.login, - })) - return {'user_ids': res} + user_model = self.pool['res.users'] + user_ids = context.get('active_model') == 'res.users' and context.get('active_ids') or [] + return [ + (0, 0, {'user_id': user.id, 'user_login': user.login}) + for user in user_model.browse(cr, uid, user_ids, context=context) + ] - def change_password_button(self, cr, uid, id, context=None): - wizard = self.browse(cr, uid, id, context=context)[0] + _defaults = { + 'user_ids': _default_user_ids, + } + + def change_password_button(self, cr, uid, ids, context=None): + wizard = self.browse(cr, uid, ids, context=context)[0] need_reload = any(uid == user.user_id.id for user in wizard.user_ids) - line_ids = [user.id for user in wizard.user_ids] + line_ids = [user.id for user in wizard.user_ids] self.pool.get('change.password.user').change_password_button(cr, uid, line_ids, context=context) - # don't keep temporary password copies in the database longer than necessary - self.pool.get('change.password.user').write(cr, uid, line_ids, {'new_passwd': False}, context=context) if need_reload: return { @@ -971,8 +968,10 @@ class change_password_user(osv.TransientModel): } def change_password_button(self, cr, uid, ids, context=None): - for user in self.browse(cr, uid, ids, context=context): - self.pool.get('res.users').write(cr, uid, user.user_id.id, {'password': user.new_passwd}) + for line in self.browse(cr, uid, ids, context=context): + line.user_id.write({'password': line.new_passwd}) + # don't keep temporary passwords in the database longer than necessary + self.write(cr, uid, ids, {'new_passwd': False}, context=context) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 87a870ccbd6a0c3e38dc063f498fda435a9e8315 Mon Sep 17 00:00:00 2001 From: Fekete Mihai Date: Wed, 6 Aug 2014 11:36:13 +0200 Subject: [PATCH 12/26] [FIX] stock: typo when selecting inventory on single product --- addons/stock/stock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 42dad7d1062..818b3ebe1af 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -2598,7 +2598,7 @@ class stock_inventory(osv.osv): domain += ' and lot_id = %s' args += (inventory.lot_id.id,) if inventory.product_id: - domain += 'and product_id = %s' + domain += ' and product_id = %s' args += (inventory.product_id.id,) if inventory.package_id: domain += ' and package_id = %s' From e106ef91efa40150c30022dff4b7cd43d06d24c9 Mon Sep 17 00:00:00 2001 From: Mohammed Shekha Date: Wed, 6 Aug 2014 15:10:46 +0530 Subject: [PATCH 13/26] [FIX] Exporting of res.partner works again Singleton object was required while access model properties, but search returns multiple results and hence caused traceback while accessing record.property --- openerp/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openerp/models.py b/openerp/models.py index 2f3b90a89b3..5280117c0d9 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -841,10 +841,10 @@ class BaseModel(object): ir_model_data = self.sudo().env['ir.model.data'] data = ir_model_data.search([('model', '=', self._name), ('res_id', '=', self.id)]) if data: - if data.module: - return '%s.%s' % (data.module, data.name) + if data[0].module: + return '%s.%s' % (data[0].module, data[0].name) else: - return data.name + return data[0].name else: postfix = 0 name = '%s_%s' % (self._table, self.id) From 6dc46766323227325422a85f433a4274183a63f5 Mon Sep 17 00:00:00 2001 From: Daniel Dico Date: Sat, 2 Aug 2014 04:03:11 -0400 Subject: [PATCH 14/26] [FIX] website_livechat: reference of demo data was preventing to install the module Added a record of im_livechat.channel in website_livechat data --- addons/website_livechat/website_livechat_data.xml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/addons/website_livechat/website_livechat_data.xml b/addons/website_livechat/website_livechat_data.xml index 55c59895664..5e37397c471 100644 --- a/addons/website_livechat/website_livechat_data.xml +++ b/addons/website_livechat/website_livechat_data.xml @@ -2,8 +2,13 @@ + + YourWebsiteWithOdoo.com + Hello, how may I help you? + + - + From dc9bcf479de49ece90bb4df97045c877e5a0e437 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Wed, 6 Aug 2014 14:59:57 +0200 Subject: [PATCH 15/26] [FIX] Allow search on `_inherits` fields. As `_inherits` fields are now handled via `related` fields (not stored, obviously), a new descriptor `searchable` has been added to `fields_get()` result to indicated if the field is searchable or not. --- addons/web/static/src/js/search.js | 2 +- openerp/fields.py | 25 +++++++++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index 566179c2a38..b686a486155 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -2037,7 +2037,7 @@ instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @ this._super(parent); this.fields = _(fields).chain() .map(function(val, key) { return _.extend({}, val, {'name': key}); }) - .filter(function (field) { return !field.deprecated && (field.store === void 0 || field.store || field.fnct_search); }) + .filter(function (field) { return !field.deprecated && field.searchable; }) .sortBy(function(field) {return field.string;}) .value(); this.attrs = {_: _, fields: this.fields, selected: null}; diff --git a/openerp/fields.py b/openerp/fields.py index e9fd90805f2..bf6e29826b5 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -408,7 +408,8 @@ class Field(object): self.depends = ('.'.join(self.related),) self.compute = self._compute_related self.inverse = self._inverse_related - self.search = self._search_related + if field._description_searchable(env): + self.search = self._search_related # copy attributes from field to self (string, help, etc.) for attr, prop in self.related_attrs: @@ -524,23 +525,27 @@ class Field(object): def get_description(self, env): """ Return a dictionary that describes the field `self`. """ desc = {'type': self.type} - # determine 'store' - if self.store: - # if the corresponding column is a function field, check the column - column = env[self.model_name]._columns.get(self.name) - desc['store'] = bool(getattr(column, 'store', True)) - else: - desc['store'] = False - # determine other attributes for attr, prop in self.description_attrs: value = getattr(self, prop) if callable(value): value = value(env) - if value: + if value is not None: desc[attr] = value + return desc # properties used by get_description() + + def _description_store(self, env): + if self.store: + # if the corresponding column is a function field, check the column + column = env[self.model_name]._columns.get(self.name) + return bool(getattr(column, 'store', True)) + return False + + def _description_searchable(self, env): + return self._description_store(env) or bool(self.search) + _description_depends = property(attrgetter('depends')) _description_related = property(attrgetter('related')) _description_company_dependent = property(attrgetter('company_dependent')) From e95fc488dbc668932f0f8f8b2223d752c4745347 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Thu, 10 Jul 2014 22:04:03 +0200 Subject: [PATCH 16/26] [IMP] fields: add mechanism to extend a selection field If a selection field is defined by a list as selection, such as: state = fields.Selection([('a', 'A'), ('b', 'B')]) one can extend it by inheritance by redefining the field, as: state = fields.Selection(selection_add=[('c', 'C')]) The result is that the selection field will have the list [('a', 'A'), ('b', 'B'), ('c', 'C')] as selection. --- openerp/addons/test_inherit/models.py | 11 +++++++++ .../addons/test_inherit/tests/test_inherit.py | 16 +++++++++---- openerp/fields.py | 23 ++++++++++++++++++- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/openerp/addons/test_inherit/models.py b/openerp/addons/test_inherit/models.py index ebbe71e0521..f620dfe53a9 100644 --- a/openerp/addons/test_inherit/models.py +++ b/openerp/addons/test_inherit/models.py @@ -7,6 +7,7 @@ class mother(models.Model): name = fields.Char('Name', required=True) surname = fields.Char(compute='_compute_surname') + state = fields.Selection([('a', 'A'), ('b', 'B')]) @api.one @api.depends('name') @@ -35,6 +36,9 @@ class mother(models.Model): # extend the name field by adding a default value name = fields.Char(default='Unknown') + # extend the selection of the state field + state = fields.Selection(selection_add=[('c', 'C')]) + # override the computed field, and extend its dependencies @api.one @api.depends('field_in_mother') @@ -44,4 +48,11 @@ class mother(models.Model): else: super(mother, self)._compute_surname() + +class mother(models.Model): + _inherit = 'test.inherit.mother' + + # extend again the selection of the state field + state = fields.Selection(selection_add=[('d', 'D')]) + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/test_inherit/tests/test_inherit.py b/openerp/addons/test_inherit/tests/test_inherit.py index b663d911f60..8039e698669 100644 --- a/openerp/addons/test_inherit/tests/test_inherit.py +++ b/openerp/addons/test_inherit/tests/test_inherit.py @@ -9,15 +9,15 @@ class test_inherits(common.TransactionCase): # is accessible from the child model. This test has been written # to verify the purpose of the inheritance computing of the class # in the openerp.osv.orm._build_model. - mother = self.registry('test.inherit.mother') - daugther = self.registry('test.inherit.daugther') + mother = self.env['test.inherit.mother'] + daugther = self.env['test.inherit.daugther'] self.assertIn('field_in_mother', mother._fields) self.assertIn('field_in_mother', daugther._fields) def test_field_extension(self): """ check the extension of a field in an inherited model """ - mother = self.registry('test.inherit.mother') + mother = self.env['test.inherit.mother'] field = mother._fields['name'] # the field should inherit required=True, and have a default value @@ -26,11 +26,19 @@ class test_inherits(common.TransactionCase): def test_depends_extension(self): """ check that @depends on overridden compute methods extends dependencies """ - mother = self.registry('test.inherit.mother') + mother = self.env['test.inherit.mother'] field = mother._fields['surname'] # the field dependencies are added self.assertItemsEqual(field.depends, ['name', 'field_in_mother']) + def test_selection_extension(self): + """ check that attribute selection_add=... extends selection on fields. """ + mother = self.env['test.inherit.mother'] + field = mother._fields['state'] + + # the extra values are added + self.assertEqual(field.selection, [('a', 'A'), ('b', 'B'), ('c', 'C'), ('d', 'D')]) + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/fields.py b/openerp/fields.py index bf6e29826b5..71ded7c4a5e 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -1116,12 +1116,16 @@ class Selection(Field): It is given as either a list of pairs (`value`, `string`), or a model method, or a method name. + :param selection_add: provides an extension of the selection in the case + of an overridden field. It is a list of pairs (`value`, `string`). + The attribute `selection` is mandatory except in the case of related fields (see :ref:`field-related`) or field extensions (see :ref:`field-incremental-definition`). """ type = 'selection' - selection = None # [(value, string), ...], model method or method name + selection = None # [(value, string), ...], function or method name + selection_add = None # [(value, string), ...] def __init__(self, selection=None, string=None, **kwargs): if callable(selection): @@ -1135,6 +1139,23 @@ class Selection(Field): field = self.related_field self.selection = lambda model: field._description_selection(model.env) + def _setup_regular(self, env): + super(Selection, self)._setup_regular(env) + # determine selection (applying extensions) + cls = type(env[self.model_name]) + selection = None + for field in resolve_all_mro(cls, self.name, reverse=True): + if isinstance(field, type(self)): + # We cannot use field.selection or field.selection_add here + # because those attributes are overridden by `set_class_name`. + if 'selection' in field._attrs: + selection = field._attrs['selection'] + if 'selection_add' in field._attrs: + selection = selection + field._attrs['selection_add'] + else: + selection = None + self.selection = selection + def _description_selection(self, env): """ return the selection list (pairs (value, label)); labels are translated according to context language From 068d4c487e02f2e727f953b1bb130a20bced4c77 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Wed, 6 Aug 2014 11:40:03 +0200 Subject: [PATCH 17/26] [IMP] models: name_search() should call _name_search() This avoids code duplication between methods, and keeps backward compatibility with existing code overriding _name_search(). --- openerp/models.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/openerp/models.py b/openerp/models.py index 5280117c0d9..8beb60d53b7 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -1729,12 +1729,7 @@ class BaseModel(object): :rtype: list :return: list of pairs ``(id, text_repr)`` for all matching records. """ - args = list(args or []) - if not self._rec_name: - _logger.warning("Cannot execute name_search, no _rec_name defined on %s", self._name) - elif not (name == '' and operator == 'ilike'): - args += [(self._rec_name, operator, name)] - return self.search(args, limit=limit).name_get() + return self._name_search(name, args, operator, limit=limit) def _name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None): # private implementation of name_search, allows passing a dedicated user From 135962184d68e278ee564502d079cb3235035015 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Wed, 6 Aug 2014 17:27:27 +0200 Subject: [PATCH 18/26] [FIX] web: make search by `id` possible. + correct tests --- addons/web/static/src/js/search.js | 2 +- addons/web/static/test/search.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index b686a486155..7ed70f7bc68 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -1963,7 +1963,7 @@ instance.web.search.Advanced = instance.web.search.Input.extend({ context: this.view.dataset.context }).done(function(data) { self.fields = { - id: { string: 'ID', type: 'id' } + id: { string: 'ID', type: 'id', searchable: true } }; _.each(data, function(field_def, field_name) { if (field_def.selectable !== false && field_name != 'id') { diff --git a/addons/web/static/test/search.js b/addons/web/static/test/search.js index d93cb131e05..828347b68a5 100644 --- a/addons/web/static/test/search.js +++ b/addons/web/static/test/search.js @@ -157,7 +157,7 @@ var makeSearchView = function (instance, dummy_widget_attributes, defaults) { return { type: 'search', fields: { - dummy: {type: 'char', string: "Dummy"} + dummy: {type: 'char', string: "Dummy", searchable: true} }, arch: '' }; @@ -168,7 +168,7 @@ var makeSearchView = function (instance, dummy_widget_attributes, defaults) { }; instance.session.responses['dummy.model:fields_get'] = function () { return { - dummy: {type: 'char', string: 'Dummy'} + dummy: {type: 'char', string: 'Dummy', searchable: true} }; }; instance.client = { action_manager: { inner_action: undefined } }; From f062eec6398e59f8636b00ef922be48a0f69c772 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 6 Aug 2014 18:48:35 +0200 Subject: [PATCH 19/26] [FIX] orm: _compute_related: ensure prefetch is done in batch Due to the use of a sudo env, the records were being added to the sudo cache one by one instead of all at once. This meant the prefetching was not able to load all records at once, leading to prohibitive times when processing thousands of records. --- openerp/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/fields.py b/openerp/fields.py index 71ded7c4a5e..4cfb2879747 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -418,9 +418,9 @@ class Field(object): def _compute_related(self, records): """ Compute the related field `self` on `records`. """ - for record in records: + for record, sudo_record in zip(records, records.sudo()): # bypass access rights check when traversing the related path - value = record.sudo() if record.id else record + value = sudo_record if record.id else record # traverse the intermediate fields, and keep at most one record for name in self.related[:-1]: value = value[name][:1] From 622b52d262f9f2aa8d0ecb4dab464416a6f65f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 6 Aug 2014 11:36:54 +0200 Subject: [PATCH 20/26] [FIX] project_issue, resource: fixed computation of hours / days to open (assign) or close an issue. Those fields now have a conditional store, because they were not recomputed even when changing date_open and date_closed. Changing the stage now correctly changes the date_closed. Also added some fixes in resource module, the date was not correctly taken into account when computing the number of working hours between two dates. --- addons/project_issue/project_issue.py | 151 ++++++++++---------- addons/project_issue/project_issue_view.xml | 22 +-- addons/resource/resource.py | 13 +- 3 files changed, 96 insertions(+), 90 deletions(-) diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py index 016313b5e44..a2fbed7492e 100644 --- a/addons/project_issue/project_issue.py +++ b/addons/project_issue/project_issue.py @@ -25,9 +25,11 @@ from openerp import api from openerp import SUPERUSER_ID from openerp import tools from openerp.osv import fields, osv, orm +from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT from openerp.tools import html2plaintext from openerp.tools.translate import _ + class project_issue_version(osv.Model): _name = "project.issue.version" _order = "name desc" @@ -129,73 +131,51 @@ class project_issue(osv.Model): @return: difference between current date and log date @param context: A standard dictionary for contextual values """ - cal_obj = self.pool.get('resource.calendar') - res_obj = self.pool.get('resource.resource') + Calendar = self.pool['resource.calendar'] - res = {} + res = dict.fromkeys(ids, dict()) for issue in self.browse(cr, uid, ids, context=context): - + values = { + 'day_open': 0.0, 'day_close': 0.0, + 'working_hours_open': 0.0, 'working_hours_close': 0.0, + 'days_since_creation': 0.0, 'inactivity_days': 0.0, + } # if the working hours on the project are not defined, use default ones (8 -> 12 and 13 -> 17 * 5), represented by None - if not issue.project_id or not issue.project_id.resource_calendar_id: - working_hours = None + calendar_id = None + if issue.project_id and issue.project_id.resource_calendar_id: + calendar_id = issue.project_id.resource_calendar_id.id + + dt_create_date = datetime.strptime(issue.create_date, DEFAULT_SERVER_DATETIME_FORMAT) + + if issue.date_open: + dt_date_open = datetime.strptime(issue.date_open, DEFAULT_SERVER_DATETIME_FORMAT) + values['day_open'] = (dt_date_open - dt_create_date).total_seconds() / (24.0 * 3600) + values['working_hours_open'] = Calendar._interval_hours_get( + cr, uid, calendar_id, dt_create_date, dt_date_open, + timezone_from_uid=issue.user_id.id or uid, + exclude_leaves=False, context=context) + + if issue.date_closed: + dt_date_closed = datetime.strptime(issue.date_closed, DEFAULT_SERVER_DATETIME_FORMAT) + values['day_close'] = (dt_date_closed - dt_create_date).total_seconds() / (24.0 * 3600) + values['working_hours_close'] = Calendar._interval_hours_get( + cr, uid, calendar_id, dt_create_date, dt_date_closed, + timezone_from_uid=issue.user_id.id or uid, + exclude_leaves=False, context=context) + + days_since_creation = datetime.today() - dt_create_date + values['days_since_creation'] = days_since_creation.days + if issue.date_action_last: + inactive_days = datetime.today() - datetime.strptime(issue.date_action_last, DEFAULT_SERVER_DATETIME_FORMAT) + elif issue.date_last_stage_update: + inactive_days = datetime.today() - datetime.strptime(issue.date_last_stage_update, DEFAULT_SERVER_DATETIME_FORMAT) else: - working_hours = issue.project_id.resource_calendar_id.id + inactive_days = datetime.today() - datetime.strptime(issue.create_date, DEFAULT_SERVER_DATETIME_FORMAT) + values['inactivity_days'] = inactive_days.days - res[issue.id] = {} + # filter only required values for field in fields: - duration = 0 - ans = False - hours = 0 - - date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S") - if field in ['working_hours_open','day_open']: - if issue.date_open: - date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S") - ans = date_open - date_create - date_until = issue.date_open - #Calculating no. of working hours to open the issue - hours = cal_obj._interval_hours_get(cr, uid, working_hours, - date_create, - date_open, - timezone_from_uid=issue.user_id.id or uid, - exclude_leaves=False, - context=context) - elif field in ['working_hours_close','day_close']: - if issue.date_closed: - date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S") - date_until = issue.date_closed - ans = date_close - date_create - #Calculating no. of working hours to close the issue - hours = cal_obj._interval_hours_get(cr, uid, working_hours, - date_create, - date_close, - timezone_from_uid=issue.user_id.id or uid, - exclude_leaves=False, - context=context) - elif field in ['days_since_creation']: - if issue.create_date: - days_since_creation = datetime.today() - datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S") - res[issue.id][field] = days_since_creation.days - continue - - elif field in ['inactivity_days']: - res[issue.id][field] = 0 - if issue.date_action_last: - inactive_days = datetime.today() - datetime.strptime(issue.date_action_last, '%Y-%m-%d %H:%M:%S') - res[issue.id][field] = inactive_days.days - continue - if ans: - resource_id = False - if issue.user_id: - resource_ids = res_obj.search(cr, uid, [('user_id','=',issue.user_id.id)]) - if resource_ids and len(resource_ids): - resource_id = resource_ids[0] - duration = float(ans.days) + float(ans.seconds)/(24*3600) - - if field in ['working_hours_open','working_hours_close']: - res[issue.id][field] = hours - elif field in ['day_open','day_close']: - res[issue.id][field] = duration + res[issue.id][field] = values[field] return res @@ -230,11 +210,12 @@ class project_issue(osv.Model): if work.task_id: issues += issue_pool.search(cr, uid, [('task_id','=',work.task_id.id)]) return issues + _columns = { 'id': fields.integer('ID', readonly=True), 'name': fields.char('Issue', required=True), 'active': fields.boolean('Active', required=False), - 'create_date': fields.datetime('Creation Date', readonly=True,select=True), + 'create_date': fields.datetime('Creation Date', readonly=True, select=True), 'write_date': fields.datetime('Update Date', readonly=True), 'days_since_creation': fields.function(_compute_day, string='Days since creation date', \ multi='compute_day', type="integer", help="Difference in days between creation date and current date"), @@ -254,9 +235,9 @@ class project_issue(osv.Model): required=False), 'email_from': fields.char('Email', size=128, help="These people will receive email.", select=1), 'email_cc': fields.char('Watchers Emails', size=256, help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"), - 'date_open': fields.datetime('Opened', readonly=True,select=True), + 'date_open': fields.datetime('Assigned', readonly=True, select=True), # Project Issue fields - 'date_closed': fields.datetime('Closed', readonly=True,select=True), + 'date_closed': fields.datetime('Closed', readonly=True, select=True), 'date': fields.datetime('Date'), 'date_last_stage_update': fields.datetime('Last Stage Update', select=True), 'channel': fields.char('Channel', help="Communication channel."), @@ -269,17 +250,21 @@ class project_issue(osv.Model): 'project_id': fields.many2one('project.project', 'Project', track_visibility='onchange', select=True), 'duration': fields.float('Duration'), 'task_id': fields.many2one('project.task', 'Task', domain="[('project_id','=',project_id)]"), - 'day_open': fields.function(_compute_day, string='Days to Open', \ - multi='compute_day', type="float", store=True), - 'day_close': fields.function(_compute_day, string='Days to Close', \ - multi='compute_day', type="float", store=True), + 'day_open': fields.function(_compute_day, string='Days to Assign', + multi='compute_day', type="float", + store={'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['date_open'], 10)}), + 'day_close': fields.function(_compute_day, string='Days to Close', + multi='compute_day', type="float", + store={'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['date_closed'], 10)}), 'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1, track_visibility='onchange'), - 'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \ - multi='compute_day', type="float", store=True), - 'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \ - multi='compute_day', type="float", store=True), - 'inactivity_days': fields.function(_compute_day, string='Days since last action', \ - multi='compute_day', type="integer", help="Difference in days between last action and current date"), + 'working_hours_open': fields.function(_compute_day, string='Working Hours to assign the Issue', + multi='compute_day', type="float", + store={'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['date_open'], 10)}), + 'working_hours_close': fields.function(_compute_day, string='Working Hours to close the Issue', + multi='compute_day', type="float", + store={'project.issue': (lambda self, cr, uid, ids, c={}: ids, ['date_closed'], 10)}), + 'inactivity_days': fields.function(_compute_day, string='Days since last action', + multi='compute_day', type="integer", help="Difference in days between last action and current date"), 'color': fields.integer('Color Index'), 'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True), 'date_action_last': fields.datetime('Last Action', readonly=1), @@ -312,13 +297,16 @@ class project_issue(osv.Model): default = {} default = default.copy() default.update(name=_('%s (copy)') % (issue['name'])) - return super(project_issue, self).copy(cr, uid, id, default=default, - context=context) + return super(project_issue, self).copy(cr, uid, id, default=default, context=context) def create(self, cr, uid, vals, context=None): context = dict(context or {}) if vals.get('project_id') and not context.get('default_project_id'): context['default_project_id'] = vals.get('project_id') + if vals.get('user_id'): + vals['date_open'] = fields.datetime.now() + if 'stage_id' in vals: + vals.update(self.onchange_stage_id(cr, uid, None, vals.get('stage_id'), context=context)['value']) # context: no_log, because subtype already handle this create_context = dict(context, mail_create_nolog=True) @@ -327,12 +315,13 @@ class project_issue(osv.Model): def write(self, cr, uid, ids, vals, context=None): # stage change: update date_last_stage_update if 'stage_id' in vals: + vals.update(self.onchange_stage_id(cr, uid, ids, vals.get('stage_id'), context=context)['value']) vals['date_last_stage_update'] = fields.datetime.now() if 'kanban_state' not in vals: vals['kanban_state'] = 'normal' # user_id change: update date_start if vals.get('user_id'): - vals['date_start'] = fields.datetime.now() + vals['date_open'] = fields.datetime.now() return super(project_issue, self).write(cr, uid, ids, vals, context) @@ -363,6 +352,14 @@ class project_issue(osv.Model): # Stage management # ------------------------------------------------------- + def onchange_stage_id(self, cr, uid, ids, stage_id, context=None): + if not stage_id: + return {'value': {}} + stage = self.pool['project.task.type'].browse(cr, uid, stage_id, context=context) + if stage.fold: + return {'value': {'date_closed': fields.datetime.now()}} + return {'value': {'date_closed': False}} + def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None): """ Override of the base.stage method Parameter of the stage search taken from the issue: diff --git a/addons/project_issue/project_issue_view.xml b/addons/project_issue/project_issue_view.xml index 502598193a6..620c612ecf1 100644 --- a/addons/project_issue/project_issue_view.xml +++ b/addons/project_issue/project_issue_view.xml @@ -90,16 +90,18 @@ - - - - - - - - - - + + + + + + + + + + + + diff --git a/addons/resource/resource.py b/addons/resource/resource.py index 5a43234ac46..b16fd210593 100644 --- a/addons/resource/resource.py +++ b/addons/resource/resource.py @@ -329,7 +329,8 @@ class resource_calendar(osv.osv): # no calendar: try to use the default_interval, then return directly if id is None: if default_interval: - intervals.append((start_dt.replace(hour=default_interval[0]), start_dt.replace(hour=default_interval[1]))) + working_interval = (start_dt.replace(hour=default_interval[0], minute=0, second=0), start_dt.replace(hour=default_interval[1], minute=0, second=0)) + intervals = self.interval_remove_leaves(working_interval, work_limits) return intervals working_intervals = [] @@ -371,10 +372,16 @@ class resource_calendar(osv.osv): resource_id=None, default_interval=None, context=None): hours = 0.0 for day in rrule.rrule(rrule.DAILY, dtstart=start_dt, - until=end_dt + datetime.timedelta(days=1), + until=(end_dt + datetime.timedelta(days=1)).replace(hour=0, minute=0, second=0), byweekday=self.get_weekdays(cr, uid, id, context=context)): + day_start_dt = day.replace(hour=0, minute=0, second=0) + if start_dt and day.date() == start_dt.date(): + day_start_dt = start_dt + day_end_dt = day.replace(hour=23, minute=59, second=59) + if end_dt and day.date() == end_dt.date(): + day_end_dt = end_dt hours += self.get_working_hours_of_date( - cr, uid, id, start_dt=day, + cr, uid, id, start_dt=day_start_dt, end_dt=day_end_dt, compute_leaves=compute_leaves, resource_id=resource_id, default_interval=default_interval, context=context) From 2ca875572acdcdb0c62c171562b1bb1f7230d593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 6 Aug 2014 13:00:06 +0200 Subject: [PATCH 21/26] [FIX] crm: fixed day_open and day_close computation (conditional store + be sure that date_open and date_closed fields are set) --- addons/crm/crm_lead.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index c4fd782bea2..d994237e094 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -229,10 +229,12 @@ class crm_lead(format_address, osv.osv): 'user_id': fields.many2one('res.users', 'Salesperson', select=True, track_visibility='onchange'), 'referred': fields.char('Referred By'), 'date_open': fields.datetime('Assigned', readonly=True), - 'day_open': fields.function(_compute_day, string='Days to Open', \ - multi='day_open', type="float", store=True), - 'day_close': fields.function(_compute_day, string='Days to Close', \ - multi='day_open', type="float", store=True), + 'day_open': fields.function(_compute_day, string='Days to Assign', + multi='day_open', type="float", + store={'crm.lead': (lambda self, cr, uid, ids, c={}: ids, ['date_open'], 10)}), + 'day_close': fields.function(_compute_day, string='Days to Close', + multi='day_open', type="float", + store={'crm.lead': (lambda self, cr, uid, ids, c={}: ids, ['date_closed'], 10)}), 'date_last_stage_update': fields.datetime('Last Stage Update', select=True), # Messaging and marketing @@ -887,6 +889,8 @@ class crm_lead(format_address, osv.osv): context['default_type'] = vals.get('type') if vals.get('section_id') and not context.get('default_section_id'): context['default_section_id'] = vals.get('section_id') + if vals.get('user_id'): + vals['date_open'] = fields.datetime.now() # context: no_log, because subtype already handle this create_context = dict(context, mail_create_nolog=True) @@ -896,7 +900,9 @@ class crm_lead(format_address, osv.osv): # stage change: update date_last_stage_update if 'stage_id' in vals: vals['date_last_stage_update'] = fields.datetime.now() - # stage change with new stage: update probability + if vals.get('user_id'): + vals['date_open'] = fields.datetime.now() + # stage change with new stage: update probability and date_closed if vals.get('stage_id') and not vals.get('probability'): onchange_stage_values = self.onchange_stage_id(cr, uid, ids, vals.get('stage_id'), context=context)['value'] vals.update(onchange_stage_values) From 0c462a12b91cc596d640c3ebe34818830ce891ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 6 Aug 2014 14:29:48 +0200 Subject: [PATCH 22/26] [FIX] mail: compose wizard: fixed default_get overrides not correctly managing the received field_list + improved custom call to default_get to correctly manage its in and out values. --- addons/email_template/wizard/mail_compose_message.py | 8 ++++++-- addons/mail/wizard/mail_compose_message.py | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/addons/email_template/wizard/mail_compose_message.py b/addons/email_template/wizard/mail_compose_message.py index ebe66757b09..f79aa77ddc9 100644 --- a/addons/email_template/wizard/mail_compose_message.py +++ b/addons/email_template/wizard/mail_compose_message.py @@ -19,7 +19,7 @@ # ############################################################################## -from openerp import tools, SUPERUSER_ID +from openerp import tools from openerp.osv import osv, fields @@ -54,6 +54,8 @@ class mail_compose_message(osv.TransientModel): res.get('model'), res.get('res_id'), context=context )['value'] ) + if fields is not None: + [res.pop(field, None) for field in res.keys() if field not in fields] return res _columns = { @@ -113,7 +115,9 @@ class mail_compose_message(osv.TransientModel): } values.setdefault('attachment_ids', list()).append(ir_attach_obj.create(cr, uid, data_attach, context=context)) else: - values = self.default_get(cr, uid, ['subject', 'body', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id'], context=context) + default_context = dict(context, default_composition_mode=composition_mode, default_model=model, default_res_id=res_id) + default_values = self.default_get(cr, uid, ['composition_mode', 'model', 'res_id', 'subject', 'body', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id'], context=default_context) + values = dict((key, default_values[key]) for key in ['subject', 'body', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id'] if key in default_values) if values.get('body_html'): values['body'] = values.pop('body_html') diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 66f843cd03a..a33982b3860 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -68,7 +68,7 @@ class mail_compose_message(osv.TransientModel): result = super(mail_compose_message, self).default_get(cr, uid, fields, context=context) # v6.1 compatibility mode - result['composition_mode'] = result.get('composition_mode', context.get('mail.compose.message.mode')) + result['composition_mode'] = result.get('composition_mode', context.get('mail.compose.message.mode', 'comment')) result['model'] = result.get('model', context.get('active_model')) result['res_id'] = result.get('res_id', context.get('active_id')) result['parent_id'] = result.get('parent_id', context.get('message_id')) @@ -97,6 +97,9 @@ class mail_compose_message(osv.TransientModel): if result['model'] == 'res.users' and result['res_id'] == uid: result['model'] = 'res.partner' result['res_id'] = self.pool.get('res.users').browse(cr, uid, uid).partner_id.id + + if fields is not None: + [result.pop(field, None) for field in result.keys() if field not in fields] return result def _get_composition_mode_selection(self, cr, uid, context=None): From a949b09c3e1afbdbc95171445004006f59a03660 Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Thu, 7 Aug 2014 09:50:16 +0200 Subject: [PATCH 23/26] [FIX] website_sale_option: make options work --- addons/website_sale_options/models/sale_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website_sale_options/models/sale_order.py b/addons/website_sale_options/models/sale_order.py index f16feaecc7b..85eaab214f7 100644 --- a/addons/website_sale_options/models/sale_order.py +++ b/addons/website_sale_options/models/sale_order.py @@ -45,7 +45,7 @@ class sale_order(osv.Model): line.write({ "name": _("%s\nOption for: %s") % (line.name, linked.product_id.name_get()[0][1]), "linked_line_id": linked_line_id - }, context=context) + }) # select linked product option_ids = [l.id for l in so.order_line if l.linked_line_id.id == line.id] From 50a67c8e9befba6efe9fee228cbbb57b5c8e6c56 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 7 Aug 2014 13:01:03 +0200 Subject: [PATCH 24/26] [FIX] fields.py: fix old field to new field convertion --- openerp/osv/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index e00204caf9e..0f532140371 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -147,7 +147,7 @@ class _column(object): ('domain', self._domain), ('context', self._context), ] - return dict(item for item in items if items[1]) + return dict(item for item in items if item[1]) def restart(self): pass From 5bd3c02becd229b386af82d7e3a54a3296995f29 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 7 Aug 2014 13:04:26 +0200 Subject: [PATCH 25/26] [FIX] fields.py: handle "change_default" and "deprecated" attributes in new fields --- openerp/fields.py | 6 ++++++ openerp/osv/fields.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/openerp/fields.py b/openerp/fields.py index 4cfb2879747..1b3cdabe29c 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -284,6 +284,8 @@ class Field(object): required = False states = None groups = False # csv list of group xml ids + change_default = None # whether the field may trigger a "user-onchange" + deprecated = None # whether the field is ... deprecated def __init__(self, string=None, **kwargs): kwargs['string'] = string @@ -553,6 +555,8 @@ class Field(object): _description_required = property(attrgetter('required')) _description_states = property(attrgetter('states')) _description_groups = property(attrgetter('groups')) + _description_change_default = property(attrgetter('change_default')) + _description_deprecated = property(attrgetter('deprecated')) def _description_string(self, env): if self.string and env.lang: @@ -604,6 +608,8 @@ class Field(object): _column_required = property(attrgetter('required')) _column_states = property(attrgetter('states')) _column_groups = property(attrgetter('groups')) + _column_change_default = property(attrgetter('change_default')) + _column_deprecated = property(attrgetter('deprecated')) ############################################################################ # diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 0f532140371..69675d41d83 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -146,6 +146,8 @@ class _column(object): ('translate', self.translate), ('domain', self._domain), ('context', self._context), + ('change_default', self.change_default), + ('deprecated', self.change_default), ] return dict(item for item in items if item[1]) From 407c1be1e8b51a4d31a2c0943dbbf125e9141d1b Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 7 Aug 2014 13:50:27 +0200 Subject: [PATCH 26/26] [FIX] fields.py: correct copy/paste error, use the right attribute --- openerp/osv/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 69675d41d83..2d9c38bbd4a 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -147,7 +147,7 @@ class _column(object): ('domain', self._domain), ('context', self._context), ('change_default', self.change_default), - ('deprecated', self.change_default), + ('deprecated', self.deprecated), ] return dict(item for item in items if item[1])