# HG changeset patch # User Etienne Ferriere <etienne.ferriere@xcg-consulting.fr> # Date 1528219539 -7200 # Tue Jun 05 19:25:39 2018 +0200 # Branch 10.0 # Node ID 5907004843f9d4672966c582f4ac04a781b405f2 # Parent 0d9c5e6020e13eb9cf0c395fb3963b6f99005ba0 Port to Odoo 10 the "MetaAnalytic" metaclass. diff --git a/MetaAnalytic.py b/MetaAnalytic.py --- a/MetaAnalytic.py +++ b/MetaAnalytic.py @@ -1,6 +1,7 @@ from odoo import SUPERUSER_ID from odoo import api from odoo import fields +from odoo import models from odoo.osv import orm from odoo.tools import config from odoo.tools import frozendict @@ -239,22 +240,29 @@ if ref_id is None: ref_id = orm_name.replace('.', '_') + "_analytic_dimension_id" + code_name = dimension.get('code_name', 'name') + code_description = dimension.get('code_description', 'description') + # To use an inherited, renamed parent field, you have to give its name. sync_parent = dimension.get('sync_parent', False) if sync_parent is True: sync_parent = nmspc.get('_parent_name', 'parent_id') + parent_column = dimension.get('parent_column', column) + + child_class = dimension.get('child_class', False) + rel_name = dimension.get('rel_name', tuple()) if rel_name is True: rel_name = u"Name" if isinstance(rel_name, basestring): - rel_name = (rel_name, 'name') + rel_name = (rel_name, code_name) rel_description = dimension.get('rel_description', tuple()) if rel_description is True: rel_description = u"Description" if isinstance(rel_description, basestring): - rel_description = (rel_description, 'description') + rel_description = (rel_description, code_description) rel_active = dimension.get('rel_active', tuple()) if rel_active is True: @@ -322,11 +330,11 @@ if rel_cols: # NOT a method nor a class member. 'self' is the analytic_code OSV. - def _record_from_code_id(self, cr, uid, ids, context=None): + def _record_from_code_id(self): """Get the entries to update from the modified codes.""" - osv = self.pool.get(orm_name) - domain = [(column, 'in', ids)] - return osv.search(cr, uid, domain, context=context) + obj = self.env.get(orm_name) + domain = [(column, 'in', self.ids)] + return obj.search(domain) for string, model_col, code_col, dtype, req, default in rel_cols: nmspc[model_col] = getattr(fields, dtype)( @@ -354,20 +362,22 @@ # We don't want that cause we set _bound_dimension_id. # Keep the old api until we fix all this module. @AddMethod(superclass) - def _setup_complete(self, cr, ids): + def _setup_complete(self): """Load or create the analytic dimension bound to the model.""" - super(superclass, self)._setup_complete(cr, ids) + super(superclass, self)._setup_complete() - data_osv = self.pool['ir.model.data'] + data_obj = self.env['ir.model.data'].sudo() try: - self._bound_dimension_id = data_osv.get_object_reference( - cr, SUPERUSER_ID, ref_module, ref_id + self._bound_dimension_id = data_obj.get_object_reference( + ref_module, ref_id )[1] except ValueError: - vals = {'name': dimension_name, 'validated': True} - self._bound_dimension_id = data_osv._update( - cr, SUPERUSER_ID, 'analytic.dimension', ref_module, vals, + vals = { + 'name': dimension_name, 'validated': True, + } + self._bound_dimension_id = data_obj._update( + 'analytic.dimension', ref_module, vals, xml_id=ref_id, noupdate=True ) @@ -376,11 +386,9 @@ # This function is called as a method and can be overridden. @AddMethod(superclass) - def _generate_code_ref_id(self, cr, uid, ids, context=None): - data_osv = self.pool['ir.model.data'] - records = self.browse(cr, uid, ids, context=None) - if not isinstance(records, list): - records = [records] + def _generate_code_ref_id(self, context=None): + data_obj = self.env['ir.model.data'] + records = self for record in records: code = record[column] @@ -397,39 +405,32 @@ 'model': 'analytic.code', 'res_id': code.id, } - data_osv.create(cr, uid, vals, context=context) + data_obj.create(vals) + + @AddMethod(superclass) + @api.model + def _get_bound_dimension_id(self): + return self.env['ir.model.data'].sudo().get_object_reference( + ref_module, ref_id + )[1] @AddMethod(superclass) - @api.cr_uid_context - @api.returns(orm_name, lambda a: a.id) - def create(self, cr, uid, vals, context=None, **kwargs): - """Create the analytic code.""" - - code_vals = {} - - if sync_parent: - cp = self._get_code_parent(cr, uid, vals, context=context) - if cp is not None: - code_vals['code_parent_id'] = cp - - # Direct changes to the 'bound analytic code' field are ignored - # unless the 'force_code_id' context key is passed as True. - force_code_id = vals.pop(column, False) + @api.model + def _create_analytic_code(self, vals, code_vals): # Will be set if a new code is created - new_code_id = False + new_code = False - if context and context.get('force_code_id', False): - self._force_code(cr, uid, force_code_id, code_vals, context) - vals[column] = force_code_id - + if use_inherits: + code_vals.update(vals) else: - if use_inherits: - code_vals.update(vals) - else: - code_vals['name'] = vals.get('name') + code_vals['name'] = vals.get(code_name) + code_vals['description'] = vals.get(code_description) - # OpenERP bug: related fields do not work properly on creation. + if code_vals.get('name'): + + # OpenERP bug: related fields do not work properly on + # creation. for rel in rel_cols: model_col, code_col = rel[1:3] if model_col in vals: @@ -437,152 +438,286 @@ elif model_col in self._defaults: code_vals[code_col] = self._defaults[model_col] - # We have to create the code separately, even with inherits. - code_osv = self.pool['analytic.code'] - code_vals['nd_id'] = self._bound_dimension_id - code_id = code_osv.create(cr, uid, code_vals, context=context) - vals[column] = code_id - new_code_id = code_id + # We have to create the code separately, even with + # inherits. + code_obj = self.env['analytic.code'] + code_vals['nd_id'] = self._get_bound_dimension_id() + code = code_obj.create(code_vals) + vals[column] = code.id + new_code = code + + return new_code, vals + + if child_class: + + @AddMethod(superclass) + @api.model + def protect_analytic_code(self, vals): + + context = self.env.context + code_vals = {} + + # Direct changes to the 'bound analytic code' field are ignored + # unless the 'force_code_id' context key is passed as True. + force_code_id = vals.pop(column, False) + + if sync_parent: + cp = self._get_code_parent(vals) + if cp is not None: + code_vals['code_parent_id'] = cp.id + + if context and context.get('force_code_id', False): + self._force_code(force_code_id, code_vals) + vals[column] = force_code_id + + return vals + + @AddMethod(superclass) + @api.multi + def create_analytic_code(self): + """Create the analytic code.""" + + code_obj = self.env['analytic.code'] + + for record in self: + + if not getattr(record, column): + + code_vals = { + 'name': getattr(record, code_name), + 'description': getattr(record, code_description), + 'nd_id': self._get_bound_dimension_id(), + 'origin_id': '{},{}'.format(self._name, record.id), + } + + if sync_parent: + cp = self._get_code_parent({ + sync_parent: getattr(record, sync_parent).id + }) + if cp is not None: + code_vals['code_parent_id'] = cp.id + + code = code_obj.create(code_vals) + + super(superclass, record).write({column: code.id}) - res = super(superclass, self).create( - cr, uid, vals, context=context, **kwargs - ) + if code_ref_ids: + self._generate_code_ref_id(record) + + return True + + else: + + @AddMethod(superclass) + def create(self, vals, **kwargs): + """Create the analytic code.""" + + context = self.env.context + code_vals = {} - if code_ref_ids: - self._generate_code_ref_id(cr, uid, res, context=context) + if sync_parent: + cp = self._get_code_parent(vals) + if cp is not None: + code_vals['code_parent_id'] = cp.id + + # Direct changes to the 'bound analytic code' field are ignored + # unless the 'force_code_id' context key is passed as True. + force_code_id = vals.pop(column, False) + + # Will be set if a new code is created + new_code = False + + if context and context.get('force_code_id', False): + self._force_code(force_code_id, code_vals) + vals[column] = force_code_id - if new_code_id: - code_osv.write(cr, uid, new_code_id, { - 'origin_id': '{},{}'.format(self._name, res), - }) + else: + new_code, vals = self._create_analytic_code( + vals, code_vals + ) + + res = super(superclass, self).create(vals, **kwargs) + + if not getattr(res, column): + + if sync_parent: + cp = self._get_code_parent(vals) + if cp is not None: + code_vals['code_parent_id'] = cp.id - return res + new_code, vals = self._create_analytic_code( + { + field: extract( + getattr(res, field), field_data['type'] + ) + for field, field_data + in res.fields_get().iteritems() + if field in ( + vals.keys() + [code_name, code_description] + ) + }, code_vals + ) + super(superclass, res).write({column: new_code.id}) + + if code_ref_ids: + self._generate_code_ref_id(res) + + if new_code: + new_code.write({ + 'origin_id': '{},{}'.format(self._name, res.id), + }) + + return res @AddMethod(superclass) - @api.cr_uid_ids_context - def write(self, cr, uid, ids, vals, context=None, **kwargs): + def write(self, vals, **kwargs): """Update the analytic code's name if it is not inherited, and its parent code if parent-child relations are synchronized. """ + context = self.env.context code_vals = {} new = False - if not isinstance(ids, (list, tuple)): - ids = [ids] + ids = self.ids if sync_parent: - cp = self._get_code_parent(cr, uid, vals, context=context) + cp = self._get_code_parent(vals) if cp is not None: - code_vals['code_parent_id'] = cp + code_vals['code_parent_id'] = cp.id + else: + parent = getattr(self, sync_parent) + if parent: + cp = getattr(parent, parent_column) + if cp: + code_vals['code_parent_id'] = cp.id # Direct changes to the 'bound analytic code' field are ignored # unless the 'force_code_id' context key is passed as True. force_code_id = vals.pop(column, False) if context and context.get('force_code_id', False): - self._force_code(cr, uid, force_code_id, code_vals, context) + self._force_code(force_code_id, code_vals) vals[column] = force_code_id elif use_inherits: vals.update(code_vals) else: - name_col = rel_name[1] if rel_name else 'name' + name_col = rel_name[1] if rel_name else code_name + description_col = \ + rel_description[1] if rel_description else code_description if name_col in vals: code_vals['name'] = vals[name_col] - records = self.browse(cr, uid, ids, context=context) - code_ids = [getattr(rec, column).id for rec in records] + if description_col in vals: + code_vals['description'] = vals[description_col] + codes = {rec: getattr(rec, column) for rec in self} + + res = super(superclass, self).write(vals, **kwargs) + + news = [] + + # If updating records with no code, create these. + if codes: + code_obj = self.env['analytic.code'] + + for rec, code in codes.iteritems(): + + rec_code_vals = code_vals.copy() + rec_vals = dict().copy() - # If updating a single record with no code, create it. - code_osv = self.pool['analytic.code'] - if code_ids == [False]: - new = ids[0] - code_vals['nd_id'] = self._bound_dimension_id - if 'name' not in code_vals: - code_vals['name'] = self.read( - cr, uid, new, [name_col], context=context - )[name_col] - code_vals['origin_id'] = '{},{}'.format(self._name, new) - vals[column] = code_osv.create( - cr, uid, code_vals, context=context - ) - elif code_vals: - code_osv.write( - cr, uid, code_ids, code_vals, context=context - ) + if 'name' not in rec_code_vals: + rec_code_vals['name'] = \ + rec.read([name_col])[0][name_col] + if 'description' not in code_vals: + rec_code_vals['description'] = ( + self.read([description_col])[0] + [description_col] + ) - res = super(superclass, self).write( - cr, uid, ids, vals, context=context, **kwargs - ) + if not code and not getattr(rec, column): + news.append(rec.id) + rec_code_vals['nd_id'] = rec._get_bound_dimension_id() + rec_code_vals['origin_id'] = \ + '{},{}'.format(self._name, rec.id) + rec_vals[column] = code_obj.create(rec_code_vals).id - if code_ref_ids and new is not False: - self._generate_code_ref_id(cr, uid, new, context=context) + super(superclass, rec).write(rec_vals, **kwargs) + + elif rec_code_vals: + code.write(rec_code_vals) + + if code_ref_ids and news is not False: + for new in news: + self._generate_code_ref_id(new) return res @AddMethod(superclass) - @api.cr_uid_ids_context - def unlink(self, cr, uid, ids, context=None, **kwargs): + def unlink(self, **kwargs): """When removing this object, remove all associated analytic codes referenced by this object. Note: the method will fail if the code is referenced by any other object due to the RESTRICT constraint. That is the intended behavior. """ - code_obj = self.pool['analytic.code'] # Find all related codes - code_ids = [ - record[column].id for record - in self.browse(cr, uid, ids, context=context) - ] + codes = self.env['analytic.code'] - res = super(superclass, self).unlink( - cr, uid, ids, context=context - ) + for record in self: + codes |= getattr(record, column) - code_obj.unlink(cr, uid, code_ids, context=context, **kwargs) + res = super(superclass, self).unlink() + + codes.unlink(**kwargs) return res @AddMethod(superclass) - def _force_code(self, cr, uid, force_code_id, code_vals, context=None): + def _force_code(self, force_code_id, code_vals): - code_osv = self.pool['analytic.code'] + code_obj = self.env['analytic.code'] if not force_code_id: raise ValueError( "An analytic code ID MUST be specified if the " "force_code_id key is enabled in the context" ) - force_code_dim = code_osv.read( - cr, uid, force_code_id, ['nd_id'], context=context - )['nd_id'][0] - if force_code_dim != self._bound_dimension_id: + force_code = code_obj.browse(force_code_id) + force_code_dim_id = force_code.nd_id.id + if force_code_dim_id != self._get_bound_dimension_id(): raise ValueError( "If specified, codes must belong to the bound " "analytic dimension {}".format(dimension_name) ) if code_vals: - code_osv.write( - cr, uid, force_code_id, code_vals, context=context - ) + force_code.write(code_vals) if sync_parent: # This function is called as a method and can be overridden. @AddMethod(superclass) - def _get_code_parent(self, cr, uid, vals, context=None): + def _get_code_parent(self, vals): """If parent_id is in the submitted values, return the analytic code of this parent, to be used as the child's code's parent. """ parent_id = vals.get(sync_parent, None) if parent_id is not None: + if parent_id: - res = self.read( - cr, uid, parent_id, [column], context=context - )[column] - return res[0] if res else False + + parent_model = \ + self.fields_get([sync_parent])[sync_parent].get( + 'relation' + ) + + if parent_model: + res = getattr( + self.env[parent_model].browse(parent_id), + parent_column + ) + + return res if res else False else: return False return None @@ -590,36 +725,32 @@ if use_code_name_methods: @AddMethod(superclass) - def name_get(self, cr, uid, ids, context=None): + def name_get(self): """Return the analytic code's name.""" - code_osv = self.pool.get('analytic.code') - code_reads = self.read(cr, uid, ids, [column], context=context) + code_reads = self.read([column]) c2m = { # Code IDs to model IDs code_read[column][0]: code_read['id'] for code_read in code_reads if code_read[column] is not False } - names = code_osv.name_get(cr, uid, c2m.keys(), context=context) + names = self.env['analytic.code'].browse(c2m.keys()).name_get() return [(c2m[cid], name) for cid, name in names if cid in c2m] @AddMethod(superclass) def name_search( - self, cr, uid, name, args=None, operator='ilike', context=None, - limit=100 + self, name, args=None, operator='ilike', limit=100 ): """Return the records whose analytic code matches the name.""" - code_osv = self.pool.get('analytic.code') - args.append(('nd_id', '=', self._bound_dimension_id)) - names = code_osv.name_search( - cr, uid, name, args, operator, context, limit - ) + code_obj = self.env.get('analytic.code') + args.append(('nd_id', '=', self._get_bound_dimension_id())) + names = code_obj.name_search(name, args, operator, limit) if not names: return [] dom = [(column, 'in', zip(*names)[0])] - ids = self.search(cr, uid, dom, context=context) - code_reads = self.read(cr, uid, ids, [column], context=context) + ids = self.search(dom) + code_reads = self.read([column]) c2m = { # Code IDs to model IDs code_read[column][0]: code_read['id'] for code_read in code_reads @@ -632,3 +763,22 @@ ] return (superclass,) + + +def extract(value, typ): + """Returns the ID, if the value is a browse record. + Returns the IDs, if the value is a browse record list. + Otherwise, returns the value. + """ + + result = value + if typ in ['many2many']: + res = value.ids + res.sort() + result = [(6, 0, list(set(res)))] + elif typ == 'many2one': + result = value.id + elif typ == 'one2many': + result = False + + return result