# HG changeset patch # User Szeka Wong <szeka.wong@xcg-consulting.fr> # Date 1542022283 -3600 # Mon Nov 12 12:31:23 2018 +0100 # Branch 11.0 # Node ID 9f7def441778338cad722d38525db587c6f475a0 # Parent afd556c47748f37361d8ef08f99c22ba4caa88fc Move MetaAnalytic file to module's root to avoid long import instructions. diff --git a/MetaAnalytic.py b/MetaAnalytic.py new file mode 100644 --- /dev/null +++ b/MetaAnalytic.py @@ -0,0 +1,719 @@ +from odoo import api, fields +from odoo.osv import orm +from odoo.tools import config, frozendict + +from .models.analytic_dimension import get_analytic_size + + +class AddMethod(object): + """Utility decorator to add methods to an object or a class.""" + + def __init__(self, obj): + self.obj = obj + + def __call__(self, func): + setattr(self.obj, func.__name__, func) + return func + + +class MetaAnalytic(orm.MetaModel): + """Allow the model to use the classes of the analytic_structure module + in a more streamlined way. + + The metaclass' behavior is specified by adding the following attributes: + + * _analytic: define the analytic structures to be used by the model. + * _dimensions: bind an analytic dimension to the model. + + A description of the syntax expected for each attribute is available in + the README file. + + Notes: + * This metaclass may directly modify attributes that are used by OpenERP, + specifically _inherits and the fields of the class. + * New superclasses are used to define or override methods, in order to + avoid interacting with the model's own method (re)definitions. + """ + + def __new__(cls, name, bases, nmspc): + + analytic = nmspc.get('_analytic', {}) + para = nmspc.get('_para_analytic', {}) + dimension = nmspc.get('_dimension', {}) + + defaults = nmspc.get('_defaults', None) + if defaults is None: + defaults = {} + nmspc['_defaults'] = defaults + + orm_name = nmspc.get('_name', None) + if orm_name is None: + orm_name = nmspc.get('_inherit') + + # Analytic fields should be defined in the _analytic attribute. + if analytic or para: + bases = cls._setup_analytic_fields( + analytic, para, defaults, orm_name, name, bases, nmspc + ) + + # The bound dimension should be defined in the _dimension attribute. + if dimension: + bases = cls._setup_bound_dimension( + dimension, defaults, orm_name, name, bases, nmspc + ) + + return super(MetaAnalytic, cls).__new__(cls, name, bases, nmspc) + + def __init__(self, name, bases, nmspc): + return super(MetaAnalytic, self).__init__(name, bases, nmspc) + + @classmethod + def _setup_analytic_fields( + cls, analytic, para, defaults, orm_name, name, bases, nmspc + ): + """Generate analytic and para-analytic fields on the model.""" + + # If _analytic uses a shortcut, convert it into a prefix-model mapping. + if analytic is True: + analytic = {'a': orm_name.replace('.', '_')} + elif isinstance(analytic, str): + analytic = {'a': analytic} + + # Create a field that will be used for replacement in the view + if analytic: + nmspc['analytic_dimensions'] = fields.Char( + string="Analytic Dimensions", + compute=api.one(lambda self: ( + setattr(self, 'analytic_dimensions', '') + )), + readonly=True, + ) + + col_pattern = '{pre}{n}_{suf}' + size = get_analytic_size() + + # Generate the fields directly into the nmspc. + all_analytic = [] + + for prefix, model_name in list(analytic.items()): + # Analytic fields + all_analytic.append((model_name, prefix, 'id')) + + for n in range(1, size + 1): + col_name = col_pattern.format(pre=prefix, n=n, suf='id') + domain_field = 'nd_id.ns{n}_id.model_name'.format(n=n) + nmspc[col_name] = fields.Many2one( + 'analytic.code', + "Generated Analytic Field", + domain=[ + (domain_field, '=', model_name), + ('view_type', '=', False), + ('disabled_per_company', '=', False), + ], + ondelete='restrict', + track_visibility='onchange', + index=True, + ) + + for key, value in list(para.items()): + # Para-analytic fields + prefix, suffix = key + model_name = value['model'] + all_analytic.append((model_name, prefix, suffix)) + if suffix == 'id': + raise ValueError("Para-analytic suffix cannot be 'id'") + + field_type = value['type'] + args = value['args'] + kwargs = value['kwargs'] + for n in range(1, size + 1): + col_name = col_pattern.format(pre=prefix, n=n, suf=suffix) + nmspc[col_name] = field_type(*args, **kwargs) + if 'default' in value: + defaults[col_name] = value['default'] + + # In order to preserve inheritance and possible overrides, work on a + # new class that inherits the given bases, then make our model class + # inherit from this class. + superclass_name = '_{name}_SuperAnalytic'.format(name=name) + # Set _register to False in order to prevent its instantiation. + superclass = type(superclass_name, bases, {'_register': False}) + + @AddMethod(superclass) + @api.model + def fields_get(self, allfields=None, attributes=None): + """Override this method to rename analytic fields.""" + + res = super(superclass, self).fields_get( + allfields=allfields, attributes=attributes, + ) + + analytic_osv = self.env['analytic.structure'] + + for model_name, prefix, suffix in all_analytic: + res = analytic_osv.analytic_fields_get( + model_name, res, prefix, suffix, + ) + + return res + + @AddMethod(superclass) + @api.model + def fields_view_get( + self, view_id=None, view_type='form', toolbar=False, submenu=False, + ): + """Override this method to hide unused analytic fields.""" + + res = super(superclass, self).fields_view_get( + view_id=view_id, view_type=view_type, toolbar=toolbar, + submenu=submenu, + ) + + analytic_osv = self.env['analytic.structure'] + + for model_name, prefix, suffix in all_analytic: + res = analytic_osv.analytic_fields_view_get( + model_name, res, prefix, suffix, + ) + + return res + + @AddMethod(superclass) + @api.model + @api.returns(orm_name, lambda value: value.id) + def create(self, vals, **kwargs): + """Performs analytic field validation""" + res = super(superclass, self).create(vals, **kwargs) + # Throws in case of error + res._validate_analytic_fields(frozendict(analytic)) + return res + + @AddMethod(superclass) + @api.multi + def write(self, vals, **kwargs): + """Performs analytic field validation""" + res = super(superclass, self).write(vals, **kwargs) + # Throws in case of error + self._validate_analytic_fields(frozendict(analytic)) + return res + + @AddMethod(superclass) + @api.multi + def _validate_analytic_fields(self, analytic): + """Validation function to validate analytic fields. + The base implementation doesn't actually do anything. + :param analytic: frozendict, analytic field parameters, such as + they would appear in the '_analytic' in the expanded + form, ie. as a prefix => model_name mapping. + :raises: Validation error when applicable. + """ + pass + + return (superclass,) + + @classmethod + def _setup_bound_dimension( + cls, dimension, defaults, orm_name, name, bases, nmspc + ): + """Bind a dimension to the model, creating a code for each record.""" + + if dimension is True: + dimension = {} + elif isinstance(dimension, str): + dimension = {'name': dimension} + + dimension_name = dimension.get('name', None) + if dimension_name is None: + dimension_name = nmspc.get('_description', False) or orm_name + + column = dimension.get('column', 'analytic_id') + + ref_module = dimension.get('ref_module', '') + + ref_id = dimension.get('ref_id', None) + 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) + + rel_name = dimension.get('rel_name', tuple()) + if rel_name is True: + rel_name = "Name" + if isinstance(rel_name, str): + rel_name = (rel_name, code_name) + + rel_description = dimension.get('rel_description', tuple()) + if rel_description is True: + rel_description = "Description" + if isinstance(rel_description, str): + rel_description = (rel_description, code_description) + + rel_active = dimension.get('rel_active', tuple()) + if rel_active is True: + rel_active = "Active" + if isinstance(rel_active, str): + rel_active = (rel_active, 'active') + + rel_view_type = dimension.get('rel_view_type', tuple()) + if rel_view_type is True: + rel_view_type = "View type" + if isinstance(rel_view_type, str): + rel_view_type = (rel_view_type, 'view_type') + + rel_disabled_per_company = dimension.get( + 'rel_disabled_per_company', tuple() + ) + if rel_disabled_per_company is True: + rel_disabled_per_company = "Disabled in my company" + if isinstance(rel_disabled_per_company, str): + rel_disabled_per_company = ( + rel_disabled_per_company, 'disabled_per_company' + ) + + # By default, only use inherits if we can be sure there is no conflict + # on the required fields 'name' and 'nd_id'. + # There can still be conflicts on analytic_code's optional fields. + use_inherits = dimension.get('use_inherits', None) + if use_inherits is None: + use_inherits = not ( + any(field in nmspc for field in ('name', 'nd_id')) or + nmspc.get('_inherits', False) or + nmspc.get('_inherit', False) + ) + + use_code_name_methods = dimension.get('use_code_name_methods', False) + + code_ref_ids = dimension.get('code_ref_ids', False) + if code_ref_ids is True: + code_ref_ids = ref_id + + code_ref_module = dimension.get('code_ref_module', '') + + if use_inherits: + inherits = nmspc.get('_inherits', {}) + inherits['analytic.code'] = column + nmspc['_inherits'] = inherits + + # Default column for the underlying analytic code. + if column not in nmspc: + nmspc[column] = fields.Many2one( + 'analytic.code', + "Bound Analytic Code", + required=True, + ondelete='restrict' + ) + + rel_cols = [ + cols for cols in [ + rel_name + ('name', 'Char', True, ''), + rel_description + ('description', 'Char', False, ''), + rel_active + ('active', 'Boolean', False, True), + rel_view_type + ('view_type', 'Boolean', False, False), + ] if len(cols) == 6 + ] + + if rel_cols: + # NOT a method nor a class member. 'self' is the analytic_code OSV. + def _record_from_code_id(self): + """Get the entries to update from the modified codes.""" + 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)( + string=string, + related=".".join([column, code_col]), + relation="analytic.code", + required=req, + ondelete='restrict', + store=True + ) + if model_col not in defaults: + defaults[model_col] = default + + # In order to preserve inheritance and possible overrides, work on a + # new class that inherits the given bases, then make our model class + # inherit from this class. + superclass_name = '_{name}_SuperDimension'.format(name=name) + # Set _register to False in order to prevent its instantiation. + superclass = type(superclass_name, bases, {'_register': False}) + + # We must keep the old api here !!!!!!! + # If we switch to the new, the method is call through a wrapper + # then, 'self' is a !#@*ing (!) object of the same type of __cls__ + # but totally temporary. + # 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): + """Load or create the analytic dimension bound to the model.""" + + super(superclass, self)._setup_complete() + + data_obj = self.env['ir.model.data'].sudo() + try: + self._bound_dimension_id = data_obj.get_object_reference( + ref_module, ref_id + )[1] + except ValueError: + vals = { + 'name': dimension_name + } + self._bound_dimension_id = data_obj._update( + 'analytic.dimension', ref_module, vals, + xml_id=ref_id, noupdate=True + ) + + if code_ref_ids: + prefix = config.get_misc('analytic', 'code_ref_prefix', False) + + # This function is called as a method and can be overridden. + @AddMethod(superclass) + def _generate_code_ref_id(self, context=None): + data_obj = self.env['ir.model.data'] + records = self + + for record in records: + code = record[column] + code_ref_id_builder = [prefix] if prefix else [] + if 'company_id' in record and record.company_id: + code_ref_id_builder.append(record.company_id.code) + code_ref_id_builder.append('ANC') + code_ref_id_builder.append(code_ref_ids) + code_ref_id_builder.append(code.name) + + vals = { + 'name': "_".join(code_ref_id_builder), + 'module': code_ref_module, + 'model': 'analytic.code', + 'res_id': code.id, + } + 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.model + def _create_analytic_code(self, vals, code_vals): + + # Will be set if a new code is created + new_code = False + + if use_inherits: + code_vals.update(vals) + else: + code_vals['name'] = vals.get(code_name) + code_vals['description'] = vals.get(code_description) + + 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: + code_vals[code_col] = vals[model_col] + 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_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 + + @AddMethod(superclass) + @api.model + @api.returns(orm_name, lambda value: value.id) + def create(self, vals, **kwargs): + """Create the analytic code.""" + + context = self.env.context + code_vals = {} + + if sync_parent: + cp = self._get_code_parent(vals) + if cp is not None and 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) + + # 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 + + 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 and cp: + code_vals['code_parent_id'] = cp.id + + new_code, vals = self._create_analytic_code( + { + field: extract( + getattr(res, field), field_data['type'] + ) + for field, field_data + in list(res.fields_get().items()) + if field in ( + list(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.multi + 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 = {} + news = [] + standard_process = False + + if sync_parent: + cp = self._get_code_parent(vals) + if cp is not None and cp: + code_vals['code_parent_id'] = cp.id + else: + parent = getattr(self, sync_parent) + if parent: + cp = getattr(parent, parent_column) + if cp is not None and 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(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 code_name + description_col = \ + rel_description[1] if rel_description else code_description + if name_col in vals: + code_vals['name'] = vals[name_col] + if description_col in vals: + code_vals['description'] = vals[description_col] + standard_process = True + + res = super(superclass, self).write(vals, **kwargs) + + # If updating records with no code, create these. + if standard_process: + code_obj = self.env['analytic.code'] + + for rec in self: + + code = getattr(rec, column) + + rec_code_vals = code_vals.copy() + rec_vals = dict().copy() + + if not rec_code_vals.get('name'): + rec_code_vals['name'] = \ + rec.read([name_col])[0][name_col] + if not rec_code_vals.get('description'): + rec_code_vals['description'] = ( + self.read([description_col])[0] + [description_col] + ) + + if not code and rec_code_vals.get('name'): + 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 + + 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) + 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. + """ + + # Find all related codes + codes = self.env['analytic.code'] + + for record in self: + codes |= getattr(record, column) + + res = super(superclass, self).unlink() + + codes.unlink(**kwargs) + + return res + + @AddMethod(superclass) + def _force_code(self, force_code_id, code_vals): + + 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 = 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: + 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, 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: + + 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 + + if use_code_name_methods: + + @AddMethod(superclass) + def name_get(self): + """Return the analytic code's name.""" + + 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 = self.env['analytic.code'].browse( + list(c2m.keys())).name_get() + return [(c2m[cid], name) for cid, name in names if cid in c2m] + + @AddMethod(superclass) + def name_search( + self, name, args=None, operator='ilike', limit=100 + ): + """Return the records whose analytic code matches the name.""" + + 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])] + records = self.search(dom) + code_reads = records.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 + } + return [ + (c2m[cid], cname) + for cid, cname in names + if cid in c2m + ] + + 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 diff --git a/__init__.py b/__init__.py --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,4 @@ # flake8: noqa from . import models - +from . import MetaAnalytic diff --git a/models/MetaAnalytic.py b/models/MetaAnalytic.py deleted file mode 100644 --- a/models/MetaAnalytic.py +++ /dev/null @@ -1,719 +0,0 @@ -from odoo import api, fields -from odoo.osv import orm -from odoo.tools import config, frozendict - -from .analytic_dimension import get_analytic_size - - -class AddMethod(object): - """Utility decorator to add methods to an object or a class.""" - - def __init__(self, obj): - self.obj = obj - - def __call__(self, func): - setattr(self.obj, func.__name__, func) - return func - - -class MetaAnalytic(orm.MetaModel): - """Allow the model to use the classes of the analytic_structure module - in a more streamlined way. - - The metaclass' behavior is specified by adding the following attributes: - - * _analytic: define the analytic structures to be used by the model. - * _dimensions: bind an analytic dimension to the model. - - A description of the syntax expected for each attribute is available in - the README file. - - Notes: - * This metaclass may directly modify attributes that are used by OpenERP, - specifically _inherits and the fields of the class. - * New superclasses are used to define or override methods, in order to - avoid interacting with the model's own method (re)definitions. - """ - - def __new__(cls, name, bases, nmspc): - - analytic = nmspc.get('_analytic', {}) - para = nmspc.get('_para_analytic', {}) - dimension = nmspc.get('_dimension', {}) - - defaults = nmspc.get('_defaults', None) - if defaults is None: - defaults = {} - nmspc['_defaults'] = defaults - - orm_name = nmspc.get('_name', None) - if orm_name is None: - orm_name = nmspc.get('_inherit') - - # Analytic fields should be defined in the _analytic attribute. - if analytic or para: - bases = cls._setup_analytic_fields( - analytic, para, defaults, orm_name, name, bases, nmspc - ) - - # The bound dimension should be defined in the _dimension attribute. - if dimension: - bases = cls._setup_bound_dimension( - dimension, defaults, orm_name, name, bases, nmspc - ) - - return super(MetaAnalytic, cls).__new__(cls, name, bases, nmspc) - - def __init__(self, name, bases, nmspc): - return super(MetaAnalytic, self).__init__(name, bases, nmspc) - - @classmethod - def _setup_analytic_fields( - cls, analytic, para, defaults, orm_name, name, bases, nmspc - ): - """Generate analytic and para-analytic fields on the model.""" - - # If _analytic uses a shortcut, convert it into a prefix-model mapping. - if analytic is True: - analytic = {'a': orm_name.replace('.', '_')} - elif isinstance(analytic, str): - analytic = {'a': analytic} - - # Create a field that will be used for replacement in the view - if analytic: - nmspc['analytic_dimensions'] = fields.Char( - string="Analytic Dimensions", - compute=api.one(lambda self: ( - setattr(self, 'analytic_dimensions', '') - )), - readonly=True, - ) - - col_pattern = '{pre}{n}_{suf}' - size = get_analytic_size() - - # Generate the fields directly into the nmspc. - all_analytic = [] - - for prefix, model_name in list(analytic.items()): - # Analytic fields - all_analytic.append((model_name, prefix, 'id')) - - for n in range(1, size + 1): - col_name = col_pattern.format(pre=prefix, n=n, suf='id') - domain_field = 'nd_id.ns{n}_id.model_name'.format(n=n) - nmspc[col_name] = fields.Many2one( - 'analytic.code', - "Generated Analytic Field", - domain=[ - (domain_field, '=', model_name), - ('view_type', '=', False), - ('disabled_per_company', '=', False), - ], - ondelete='restrict', - track_visibility='onchange', - index=True, - ) - - for key, value in list(para.items()): - # Para-analytic fields - prefix, suffix = key - model_name = value['model'] - all_analytic.append((model_name, prefix, suffix)) - if suffix == 'id': - raise ValueError("Para-analytic suffix cannot be 'id'") - - field_type = value['type'] - args = value['args'] - kwargs = value['kwargs'] - for n in range(1, size + 1): - col_name = col_pattern.format(pre=prefix, n=n, suf=suffix) - nmspc[col_name] = field_type(*args, **kwargs) - if 'default' in value: - defaults[col_name] = value['default'] - - # In order to preserve inheritance and possible overrides, work on a - # new class that inherits the given bases, then make our model class - # inherit from this class. - superclass_name = '_{name}_SuperAnalytic'.format(name=name) - # Set _register to False in order to prevent its instantiation. - superclass = type(superclass_name, bases, {'_register': False}) - - @AddMethod(superclass) - @api.model - def fields_get(self, allfields=None, attributes=None): - """Override this method to rename analytic fields.""" - - res = super(superclass, self).fields_get( - allfields=allfields, attributes=attributes, - ) - - analytic_osv = self.env['analytic.structure'] - - for model_name, prefix, suffix in all_analytic: - res = analytic_osv.analytic_fields_get( - model_name, res, prefix, suffix, - ) - - return res - - @AddMethod(superclass) - @api.model - def fields_view_get( - self, view_id=None, view_type='form', toolbar=False, submenu=False, - ): - """Override this method to hide unused analytic fields.""" - - res = super(superclass, self).fields_view_get( - view_id=view_id, view_type=view_type, toolbar=toolbar, - submenu=submenu, - ) - - analytic_osv = self.env['analytic.structure'] - - for model_name, prefix, suffix in all_analytic: - res = analytic_osv.analytic_fields_view_get( - model_name, res, prefix, suffix, - ) - - return res - - @AddMethod(superclass) - @api.model - @api.returns(orm_name, lambda value: value.id) - def create(self, vals, **kwargs): - """Performs analytic field validation""" - res = super(superclass, self).create(vals, **kwargs) - # Throws in case of error - res._validate_analytic_fields(frozendict(analytic)) - return res - - @AddMethod(superclass) - @api.multi - def write(self, vals, **kwargs): - """Performs analytic field validation""" - res = super(superclass, self).write(vals, **kwargs) - # Throws in case of error - self._validate_analytic_fields(frozendict(analytic)) - return res - - @AddMethod(superclass) - @api.multi - def _validate_analytic_fields(self, analytic): - """Validation function to validate analytic fields. - The base implementation doesn't actually do anything. - :param analytic: frozendict, analytic field parameters, such as - they would appear in the '_analytic' in the expanded - form, ie. as a prefix => model_name mapping. - :raises: Validation error when applicable. - """ - pass - - return (superclass,) - - @classmethod - def _setup_bound_dimension( - cls, dimension, defaults, orm_name, name, bases, nmspc - ): - """Bind a dimension to the model, creating a code for each record.""" - - if dimension is True: - dimension = {} - elif isinstance(dimension, str): - dimension = {'name': dimension} - - dimension_name = dimension.get('name', None) - if dimension_name is None: - dimension_name = nmspc.get('_description', False) or orm_name - - column = dimension.get('column', 'analytic_id') - - ref_module = dimension.get('ref_module', '') - - ref_id = dimension.get('ref_id', None) - 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) - - rel_name = dimension.get('rel_name', tuple()) - if rel_name is True: - rel_name = "Name" - if isinstance(rel_name, str): - rel_name = (rel_name, code_name) - - rel_description = dimension.get('rel_description', tuple()) - if rel_description is True: - rel_description = "Description" - if isinstance(rel_description, str): - rel_description = (rel_description, code_description) - - rel_active = dimension.get('rel_active', tuple()) - if rel_active is True: - rel_active = "Active" - if isinstance(rel_active, str): - rel_active = (rel_active, 'active') - - rel_view_type = dimension.get('rel_view_type', tuple()) - if rel_view_type is True: - rel_view_type = "View type" - if isinstance(rel_view_type, str): - rel_view_type = (rel_view_type, 'view_type') - - rel_disabled_per_company = dimension.get( - 'rel_disabled_per_company', tuple() - ) - if rel_disabled_per_company is True: - rel_disabled_per_company = "Disabled in my company" - if isinstance(rel_disabled_per_company, str): - rel_disabled_per_company = ( - rel_disabled_per_company, 'disabled_per_company' - ) - - # By default, only use inherits if we can be sure there is no conflict - # on the required fields 'name' and 'nd_id'. - # There can still be conflicts on analytic_code's optional fields. - use_inherits = dimension.get('use_inherits', None) - if use_inherits is None: - use_inherits = not ( - any(field in nmspc for field in ('name', 'nd_id')) or - nmspc.get('_inherits', False) or - nmspc.get('_inherit', False) - ) - - use_code_name_methods = dimension.get('use_code_name_methods', False) - - code_ref_ids = dimension.get('code_ref_ids', False) - if code_ref_ids is True: - code_ref_ids = ref_id - - code_ref_module = dimension.get('code_ref_module', '') - - if use_inherits: - inherits = nmspc.get('_inherits', {}) - inherits['analytic.code'] = column - nmspc['_inherits'] = inherits - - # Default column for the underlying analytic code. - if column not in nmspc: - nmspc[column] = fields.Many2one( - 'analytic.code', - "Bound Analytic Code", - required=True, - ondelete='restrict' - ) - - rel_cols = [ - cols for cols in [ - rel_name + ('name', 'Char', True, ''), - rel_description + ('description', 'Char', False, ''), - rel_active + ('active', 'Boolean', False, True), - rel_view_type + ('view_type', 'Boolean', False, False), - ] if len(cols) == 6 - ] - - if rel_cols: - # NOT a method nor a class member. 'self' is the analytic_code OSV. - def _record_from_code_id(self): - """Get the entries to update from the modified codes.""" - 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)( - string=string, - related=".".join([column, code_col]), - relation="analytic.code", - required=req, - ondelete='restrict', - store=True - ) - if model_col not in defaults: - defaults[model_col] = default - - # In order to preserve inheritance and possible overrides, work on a - # new class that inherits the given bases, then make our model class - # inherit from this class. - superclass_name = '_{name}_SuperDimension'.format(name=name) - # Set _register to False in order to prevent its instantiation. - superclass = type(superclass_name, bases, {'_register': False}) - - # We must keep the old api here !!!!!!! - # If we switch to the new, the method is call through a wrapper - # then, 'self' is a !#@*ing (!) object of the same type of __cls__ - # but totally temporary. - # 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): - """Load or create the analytic dimension bound to the model.""" - - super(superclass, self)._setup_complete() - - data_obj = self.env['ir.model.data'].sudo() - try: - self._bound_dimension_id = data_obj.get_object_reference( - ref_module, ref_id - )[1] - except ValueError: - vals = { - 'name': dimension_name - } - self._bound_dimension_id = data_obj._update( - 'analytic.dimension', ref_module, vals, - xml_id=ref_id, noupdate=True - ) - - if code_ref_ids: - prefix = config.get_misc('analytic', 'code_ref_prefix', False) - - # This function is called as a method and can be overridden. - @AddMethod(superclass) - def _generate_code_ref_id(self, context=None): - data_obj = self.env['ir.model.data'] - records = self - - for record in records: - code = record[column] - code_ref_id_builder = [prefix] if prefix else [] - if 'company_id' in record and record.company_id: - code_ref_id_builder.append(record.company_id.code) - code_ref_id_builder.append('ANC') - code_ref_id_builder.append(code_ref_ids) - code_ref_id_builder.append(code.name) - - vals = { - 'name': "_".join(code_ref_id_builder), - 'module': code_ref_module, - 'model': 'analytic.code', - 'res_id': code.id, - } - 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.model - def _create_analytic_code(self, vals, code_vals): - - # Will be set if a new code is created - new_code = False - - if use_inherits: - code_vals.update(vals) - else: - code_vals['name'] = vals.get(code_name) - code_vals['description'] = vals.get(code_description) - - 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: - code_vals[code_col] = vals[model_col] - 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_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 - - @AddMethod(superclass) - @api.model - @api.returns(orm_name, lambda value: value.id) - def create(self, vals, **kwargs): - """Create the analytic code.""" - - context = self.env.context - code_vals = {} - - if sync_parent: - cp = self._get_code_parent(vals) - if cp is not None and 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) - - # 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 - - 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 and cp: - code_vals['code_parent_id'] = cp.id - - new_code, vals = self._create_analytic_code( - { - field: extract( - getattr(res, field), field_data['type'] - ) - for field, field_data - in list(res.fields_get().items()) - if field in ( - list(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.multi - 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 = {} - news = [] - standard_process = False - - if sync_parent: - cp = self._get_code_parent(vals) - if cp is not None and cp: - code_vals['code_parent_id'] = cp.id - else: - parent = getattr(self, sync_parent) - if parent: - cp = getattr(parent, parent_column) - if cp is not None and 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(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 code_name - description_col = \ - rel_description[1] if rel_description else code_description - if name_col in vals: - code_vals['name'] = vals[name_col] - if description_col in vals: - code_vals['description'] = vals[description_col] - standard_process = True - - res = super(superclass, self).write(vals, **kwargs) - - # If updating records with no code, create these. - if standard_process: - code_obj = self.env['analytic.code'] - - for rec in self: - - code = getattr(rec, column) - - rec_code_vals = code_vals.copy() - rec_vals = dict().copy() - - if not rec_code_vals.get('name'): - rec_code_vals['name'] = \ - rec.read([name_col])[0][name_col] - if not rec_code_vals.get('description'): - rec_code_vals['description'] = ( - self.read([description_col])[0] - [description_col] - ) - - if not code and rec_code_vals.get('name'): - 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 - - 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) - 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. - """ - - # Find all related codes - codes = self.env['analytic.code'] - - for record in self: - codes |= getattr(record, column) - - res = super(superclass, self).unlink() - - codes.unlink(**kwargs) - - return res - - @AddMethod(superclass) - def _force_code(self, force_code_id, code_vals): - - 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 = 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: - 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, 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: - - 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 - - if use_code_name_methods: - - @AddMethod(superclass) - def name_get(self): - """Return the analytic code's name.""" - - 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 = self.env['analytic.code'].browse( - list(c2m.keys())).name_get() - return [(c2m[cid], name) for cid, name in names if cid in c2m] - - @AddMethod(superclass) - def name_search( - self, name, args=None, operator='ilike', limit=100 - ): - """Return the records whose analytic code matches the name.""" - - 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])] - records = self.search(dom) - code_reads = records.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 - } - return [ - (c2m[cid], cname) - for cid, cname in names - if cid in c2m - ] - - 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 diff --git a/models/__init__.py b/models/__init__.py --- a/models/__init__.py +++ b/models/__init__.py @@ -38,7 +38,6 @@ config.parser.error("\n * ".join(errors)) -from . import MetaAnalytic from . import analytic_code from . import analytic_dimension from . import analytic_structure