class InformalReference(models.Model): _name = 'informal.reference' table_name = fields.Char( 'Table Name', size=128, required=True, help='Name of DB table where informal reference are.') id_field_name = fields.Char( 'Id Field Name', size=128, required=True, default='res_id', help='Name of field where destination id are saved.') model_field_name = fields.Char( 'Model Field Name', size=128, required=True, default='res_model', help='Name of field where destination model are saved.') model_field_value = fields.Selection( MODEL_FIELD_VALUE_SELECTION, 'Model Field Value', required=True, default='0', help='How save destination model reference.')
class Adjustment(models.TransientModel): _name = 'xopgi.unrealized_gl_adjustment' wizard = fields.Many2one('xopgi.unrealized_gl_wizard') account = fields.Many2one('account.account') account_name = fields.Char(related="account.name") account_code = fields.Char(related="account.code") account_currency = fields.Many2one(related="account.currency_id") foreign_balance = fields.Float(compute='_compute_all', default=0) balance = fields.Float(compute='_compute_all', default=0) adjusted_balance = fields.Float(compute='_compute_all', default=0) gainloss = fields.Float(compute='_compute_all', default=0) @api.depends('account', 'wizard') def _compute_all(self): precision = DecimalPrecision.precision_get('Account') company_currency = self.env.user.company_id.currency_id # Map records to accounts so that we can compute the balances in a single # DB query account_map = dict(zip(self.mapped('account.id'), self)) assert len(account_map) == len(self) close_date = self[0].wizard.close_date tables, where_clause, where_params = AccountMoveLine.with_context( state='posted', date_to=close_date)._query_get() if not tables: tables = '"account_move_line"' if where_clause.strip(): filters = [where_clause] else: filters = [] filters.append('"account_move_line"."account_id" IN %s') where_params.append(tuple(account_map.keys())) query = (''' SELECT account_id AS id, COALESCE(SUM(debit), 0) - COALESCE(SUM(credit), 0) AS balance, COALESCE(SUM(amount_currency), 0) as foreign_balance FROM {tables} WHERE {filters} GROUP BY account_id ''').format(tables=tables, filters=' AND '.join(filters)) self.env.cr.execute(query, where_params) for row in self.env.cr.dictfetchall(): record = account_map.pop(int( row['id'])) # cast to int, otherwise KeyError account = record.account record.balance = balance = row['balance'] record.foreign_balance = row['foreign_balance'] record.adjusted_balance = adjusted = account.currency_id.with_context( date=close_date).compute( record.foreign_balance, company_currency, round=False, ) record.gainloss = round(adjusted - balance, precision) for record in account_map.values(): record.balance = record.foreign_balance = 0 record.adjusted_balance = record.gainloss = 0
class ProductPricelistType(models.Model): _name = "product.pricelist.type" _description = "Pricelist Type" name = fields.Char(string='Name', required=True, translate=True) key = fields.Char( string='Key', required=True, help= "Used in the code to select specific prices based on the context. Keep unchanged." )
class XopgiBoardWidget(models.Model): _name = WIDGET_MODEL_NAME _description = "Board Widget" _order = 'category, name' name = fields.Char(translate=True) category = fields.Many2one('ir.module.category') template_name = fields.Char(required=True) xml_template = fields.Text(translate=True) python_code = fields.Text() @api.multi def name_get(self): '''Returns a list with id, name of widgets or name's template ''' return [(item.id, item.name or item.template_name) for item in self] def get_widgets_dict(self): '''Returns a dictionary list that represents the widgets that the user has access to. ''' widgets = self.env[WIDGET_REL_MODEL_NAME].get_widgets() logger.debug('Widgets to show %r' % [w['name'] for w in widgets]) today = normalize_datetime(fields.Date.today(self)) for widget in widgets: self._eval_python_code(widget, today) return widgets def _eval_python_code(self, widget, today): '''Evaluate the python code of a widget ''' python_code = widget.get('python_code', '') if not python_code: return name = widget.get('name', '') env = self.env local_dict = locals() local_dict.update(globals().get('__builtins__', {})) try: logger.debug('Starting evaluation of Python code for widget %s' % name) safe_eval(python_code, local_dict, mode='exec', nocopy=True) logger.debug('Python code for widget %s evaluated sussefully.' % name) except ValueError: logger.exception( 'An error happen trying to execute the Python ' 'code for \'%s\' board widget, python code: %s', name, python_code) widget.update(local_dict.get('result', {}))
class PartnerClassification(models.Model): _name = 'res.partner.classification' name = fields.Char(required=True, translate=True) partners = fields.Many2many('res.partner', 'res_partner_classification_rel', 'classification_id', 'partner_id')
class B(models.Model): _name = 'model.b' name = fields.Char() modela_id = fields.Many2one('model.a', 'Merge') melda_ids = fields.Many2many(comodel_name='model.a', column1='meld_column1_id', column2='meld_column2_id', string='relation') partner_id = fields.Many2one('res.partner', 'Parent')
class PriceType(models.Model): """The price type is used to points which field in the product form is a price and in which currency is this price expressed. When a field is a price, you can use it in pricelists to base sale and purchase prices based on some fields of the product. """ _name = "product.price.type" _description = "Price Type" @api.model def _get_currency(self): comp = self.env.user.company_id if not comp: comp = self.env['res.company'].search([], limit=1) return comp.currency_id.id active = fields.Boolean( string="Active", default=True ) name = fields.Char( string='Price Name', required=True, translate=True, help="Name of this kind of price." ) field = fields.Selection( selection="_price_field_get", string="Product Field", size=32, required=True, help="Associated field in the product form." ) currency_id = fields.Many2one( comodel_name='res.currency', string="Currency", required=True, default=_get_currency, help="The currency the field is expressed in." ) @api.model def _price_field_get(self): mf = self.env['ir.model.fields'] fields = mf.search( [('model', 'in', (('product.product'), ('product.template'))), ('ttype', '=', 'float')] ) res = [] for field in fields: if not (field.name, field.field_description) in res: res.append((field.name, field.field_description)) return res
class ControlVariableTemplate(models.Model): '''The template is the key for get a value in a control variable: E.g. env['{model}'].browse({instance}).{field} -> One instance field value ''' _name = 'cdr.control.variable.template' name = fields.Char( translate=True ) reusable = fields.Boolean( default=True ) definition = fields.Text( help="Python code string. Allow format string arguments in it." ) args_need = fields.Boolean( help="Marc if definition need to be formatted." ) eval_mode = fields.Selection( [('eval', 'Eval'), ('exec', 'Execute')], default='eval' ) @api.onchange('reusable') def onchange_reusable(self): if not self.reusable: self.args_need = False def compile(self, values): '''Compiles the expression with `values`.''' source = self.definition if self.args_need: source = source.format(**values) compile(source, '<cdr-variable>', self.eval_mode) # TODO: safe_compile return source def eval(self, now, values): """Evaluate template definition with given param values. :param now: datetime of evaluation cycle start. :param values: param values to passe it to str.format() on definition. """ code = self.compile(values) return evaluate(code, self.eval_mode, now=now, env=self.env)
class Value(models.AbstractModel): '''A variant value mixin. Contains a single value that it represented a VARCHAR to the DB but is a Python object. Any object whose `repr()` result is sensible can be stored/retrieved. Provides `value`:attr: with the repr'ed representation and `result`:attr: with the Python object. ''' _name = 'cdr.value' value = fields.Char( # This stores the representation (result of repr) of the value. You # may use `result` to read/update this field. If you use Odoo's # write/create, or set this field directly you MUST ensure to pass the # result of `repr()`. The `write/create` methods below ensure the # writing to 'result' works. help='Value of the evaluation') @api.multi def write(self, values): # Makes writing to 'result' work. If you pass both 'value' and # 'result', 'result' wins. if 'result' in values: values['value'] = repr(values.pop('result')) return super(Value, self).write(values) @api.model def create(self, values): # Makes writing to 'result' work. If you pass both 'value' and # 'result', 'result' wins. if 'result' in values: values['value'] = repr(values.pop('result')) return super(Value, self).create(values) def _get_result(self): return safe_eval(self.value) if self.value else self.value def _set_result(self, value): self.value = repr(value) def _del_result(self): self.value = None result = fields.Property( getter=_get_result, setter=_set_result, deleter=_del_result, ) del _get_result, _set_result, _del_result
class MailServer(models.Model): # Adds the delivered column _inherit = 'ir.mail_server' delivered_address = fields.Char( 'Delivered to', required=False, help=('The email address that should be check as the ' 'recipient of this server. This way we can match this ' 'server for outgoing messages in response to emails ' 'delivered to this address.'), )
class MailAlias(models.Model): _inherit = 'mail.alias' custom_domain = fields.Char('Alias domain') alias_domain = fields.Char(compute='_compute_alias_domain', inverse='_set_alias_domain', search='_search_alias_domain', string="Alias domain", default=get_default_alias_domain) @api.multi def _compute_alias_domain(self): default_domain = get_default_alias_domain(self) for record in self: record.alias_domain = record.custom_domain or default_domain @api.model def _search_alias_domain(self, args): res = [] for cond in args: new_arg = cond if len(new_arg) == 3 and new_arg[0] == 'alias_domain': new_arg = ['custom_domain'] + list(cond)[1:] res.append(new_arg) return res @api.multi def _set_alias_domain(self): for record in self: self.custom_domain = self.alias_domain def fields_get(self, *args, **kargs): # Hack to make alias_domain editable. I don't know why it's not! result = super(MailAlias, self).fields_get(*args, **kargs) alias_domain = result.get('alias_domain', None) if alias_domain: alias_domain['readonly'] = 0 return result
class SelectableView(models.Model): _name = 'xopgi.selectable.view' _rec_name = 'label' _order = 'priority' label = fields.Char(translate=True, required=True) model_id = fields.Selection( string='Model', selection=lambda self: get_model_selection(self), required=True) view = fields.Many2one('ir.ui.view', required=True) priority = fields.Integer(default=16) def get_views(self, model): domain = [('model_id', '=', model)] return self.search(domain) def get_action(self, model=None, target='current', res_id='None'): """ Return an ir.actions.act_window """ res = dict(target=target) if self: view = self[0] model = view.model_id if res_id is not None: # If the recordset is empty in Odoo8 it returns an empty list.In # odoo10 gives error. values_action = self.env[model].browse(res_id).get_access_action() # If values_action contains a list it is because get_acess_action # was executed #in odoo8 and returns a [{}], in odoo10 returns a {}. if isinstance(values_action, list): values_action = values_action[0] res = dict(values_action, **res) else: values = { 'type': 'ir.actions.act_window', 'res_model': model, 'view_type': 'form', 'view_mode': 'form', 'views': [(self.view.id, 'form')], 'context': self._context } res = dict(values, **res) return res @api.multi def try_selected_view(self): return self.get_action(target='new')
class A(models.Model): _name = 'model.a' add_char = fields.Char() add_text = fields.Text() price_int = fields.Integer() cost_float = fields.Float() min_int = fields.Integer() max_int = fields.Integer() active = fields.Boolean(default=True) parent_id = fields.Many2one('model.a', 'Parent') meldb_ids = fields.Many2many(comodel_name='model.b', column1='meld_column2_id', column2='meld_column1_id', string='relation')
class CDRIdentifier(models.Model): _name = 'cdr.identifier' _inherit = ['cdr.value'] name = fields.Char(required=True, help="Must be a available python identifier.\n" "Just letters, digit (never in first position) " "and underscore are allowed.") evaluations = fields.One2many('cdr.history', 'identifier') _sql_constraints = [('identifier_name_unique', 'unique(name)', 'Name already exists')] @api.constrains('name') def check_name(self): '''Check name of a CDRIdentifier ''' if not check_identifier(self.name): raise ValidationError( _("Name must be a available python identifier"))
class mail_message(models.Model): """Store the translated name of the model that the message reference to.""" _name = _inherit = get_modelname(MailMessage) model_names = fields.Char( compute='_get_model_names', search='_search_model_names', string='Associated to', ) @api.multi def _get_model_names(self): for message in self: if message.model: translations = translate_model_name( self, message['model'], ) message.model_names = ' | '.join(translations) @api.model def _search_model_names(self, operator, value): ir_translation = self.env['ir.translation'] translations = ir_translation.search( [ ('name', '=', 'ir.model,name'), # Search in model names '|', # If any of: ('src', operator, value), # source matches value ('value', operator, value), # translation matches value ], ) model_names = list({trans.src for trans in translations}) models = [ m.model for m in self.env['ir.model'].search([('name', 'in', model_names)]) ] return [('model', 'in', models)]
class object_merger(models.TransientModel): _name = 'object.merger' _description = 'Merge objects' name = fields.Char('Name', size=16) @api.model def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): res = super(object_merger, self).fields_view_get(view_id, view_type, toolbar=toolbar, submenu=submenu) if view_type == 'form': object_ids = self._context.get('active_ids', []) active_model = self._context['active_model'] if object_ids: self._check_quantity(active_model) field_name = 'x_%s_id' % active_model.replace('.', '_') view_part = """ <field name="{field_name}"/> <separator string="{string}" colspan="4"/> <field name="info" nolabel="1" colspan="4"/> """.format(field_name=field_name, string=_("To merge")) res['arch'] = res['arch'].decode('utf8').replace( """<separator string="to_replace"/>""", view_part) field = self.fields_get([field_name, 'info']) field_data = field[field_name] field_data.update( domain=[('id', '=', object_ids)], selection=self.env[active_model].sudo().name_get(), required=True) res['fields'].update(field) return res if MAJOR_ODOO_VERSION < 10: @api.model def fields_get(self, fields_list=None, write_access=True, attributes=None): res = super(object_merger, self).fields_get(allfields=fields_list, write_access=write_access, attributes=attributes) if fields_list and 'info' in fields_list: active_model = self._context.get('active_model') fvg = lambda view: self.env[active_model].fields_view_get( view_type=view) res.update( info=dict(type='many2many', relation=active_model, views={k: fvg(k) for k in ['tree', 'form']})) return res else: @api.model def fields_get(self, fields_list=None, attributes=None): res = super(object_merger, self).fields_get(allfields=fields_list, attributes=attributes) if fields_list and 'info' in fields_list: active_model = self._context.get('active_model') fvg = lambda view: self.env[active_model].fields_view_get( view_type=view) res.update( info=dict(type='many2many', relation=active_model, views={k: fvg(k) for k in ['tree', 'form']})) return res @api.model def default_get(self, fields_list): res = super(object_merger, self).default_get(fields_list) if fields_list and 'info' in fields_list: res.update(info=self._context.get('active_ids', False)) return res def _check_quantity(self, active_model): '''Check for groups_id of uid and if it are not merger manager check limit quantity of objects to merge. ''' if len(self._context.get('active_ids', [])) < 2: raise UserError(_('At less two items are necessary for merge.')) model = IrModel.sudo().search([('model', '=', active_model)], limit=1) if model and 0 < model.merge_limit < len( self._context.get('active_ids', [])): raise UserError(_('You can`t merge so much objects at one time.')) return True @api.requires_singleton def action_merge(self): active_model = self._context.get('active_model') if not active_model: raise UserError(_('The is no active model defined!')) Model = self.env[active_model] sources = Model.browse(self._context.get('active_ids', [])) attr = self._context.get('field_to_read') val = getattr(self, attr, None) if attr else None if not val: raise UserError(_('Please select one value to keep')) target = val[0] self.merge(sources, target) try: Model.read([]) return target.get_formview_action() except AccessError: return CLOSE_WINDOW @api.model def merge(self, sources, target=None, deactivate=True): '''Merge all recordsets from `source` into `target`. If `target` is None, it defaults to the last-created record from `source`. If `deactivate` is True (the default), and the `sources` have an 'active' field, we deactivate the sources instead of deleting them. Return False if no merge happened. Otherwise, return True. .. rubric:: Merging trees. .. rubric:: Merging arbitrary graphs. ''' sources = sources.sudo().exists() active_model = sources._name merge_way = IrModel.search([('model', '=', active_model)]) if not target: target = sources.sorted(key=lambda date: date.create_date)[-1] sources -= target if sources: merge_way._merge(sources, target) self._check_fks(sources, target) self._check_references(sources, target) has_active = sources._fields.get('active', False) if has_active and deactivate: sources.write({'active': False}) else: sources.unlink() return True else: return False def _get_contraints(self, table, field): '''Get unique constraints that involve `column` in `table`. :return: list of list of columns per constraint where `field` is involved. ''' self._cr.execute( """ SELECT kcu.column_name, tc.constraint_name FROM information_schema.key_column_usage kcu, information_schema.key_column_usage kcuw, information_schema.table_constraints tc WHERE kcu.constraint_catalog = tc.constraint_catalog AND kcu.constraint_schema = tc.constraint_schema AND kcu.constraint_name = tc.constraint_name AND kcuw.constraint_catalog = tc.constraint_catalog AND kcuw.constraint_schema = tc.constraint_schema AND kcuw.constraint_name = tc.constraint_name AND tc.constraint_type = 'UNIQUE' AND tc.table_name = %s AND kcuw.column_name = %s """, (table, field)) constraints = {} for column_name, constraint_name in self._cr.fetchall(): data = constraints.setdefault(constraint_name, []) data.append(column_name) return constraints.values() def _check_constraints(self, table, field, target, filters, model='', subquery='{field} = {id}'): '''Check unique constraints. :return: False if any violation detected else True ''' constraints = self._get_contraints(table, field) for columns in constraints: if columns: query_filters = ' AND '.join( ['%s %s %s' % f for f in filters] + [subquery.format(field=field, model=model, id=target.id)]) self._cr.execute( "SELECT {field} FROM {table} WHERE {filters}".format( field=field, table=table, filters=query_filters)) if self._cr.rowcount: return False return True def _upd_del(self, table, field, target, filters, sources, model=None, subquery='{field}={id}'): ''' Update or Delete rows matching with filters passed. :param action_update: if True Update query are executed else Delete query are executed :return: ''' ok = self._check_constraints(table, field, target, filters, model=model, subquery=subquery) query_filters = ' AND '.join( ['%s %s %s' % f for f in filters] + [subquery.format(field=field, id=sources)]) if ok: query = "UPDATE {table} SET " + subquery + " WHERE {filters};" self._cr.execute( query.format(table=table, field=field, model=model, id=str(target.id), filters=query_filters)) else: query = """DELETE FROM {table} WHERE {filters};""" self._cr.execute(query.format(table=table, filters=query_filters)) @api.model def _check_fks(self, sources, target): '''Get all relational field with active_model and send to update it. ''' query = """ SELECT name, model, ttype FROM ir_model_fields WHERE relation=%(model)s AND ttype != 'one2many'""" args = dict(model=sources._name) model = IrModel.search([('model', '=', sources._name)]) record = sources + target if model: self._cr.execute(query, args) fks = self._cr.fetchall() else: fks = [] for field_name, model_name, ttype in fks: # When a model disappears, Odoo won't remove it from # ir_models_fields until the addon is removed. So we can get a # model_name that it's not in the registry. Ignore it. model_registry = self.env.registry.get(model_name) if not model_registry: continue # noqa if not getattr(model_registry, '_auto', False): # The model is not created by the ORM continue # noqa field = model_registry._fields.get(field_name, False) assert not field or field.relational if not field or not field.store: # Fields which are not in the continue # noqa if ttype == 'many2one': if hasattr(model_registry, '_table'): table = model_registry._table else: table = model_name.replace('.', '_') elif ttype == 'many2many': if MAJOR_ODOO_VERSION < 10: modelname = self.env[model_name] table, _x, field_name = field.column._sql_names(modelname) else: table, field_name = field.relation, field.column2 else: assert False self._upd_fk(table=table, field=field_name, sources=sources, target=target) field_names = [] field_names.append(str(field_name)) record._validate_fields(field_names) def _upd_fk(self, table, field, sources, target): '''Update foreign key (field) to destination value `target` where actual field value are in merging object `sources`. if not constraint involve the field to update. Update all matching rows else check one by one if can update and update it or delete. ''' constraints = self._get_contraints(table, field) if not constraints: query = 'UPDATE {table} SET {field}={target} WHERE {field} IN ({filter});' self._cr.execute( query.format(table=table, field=field, target=target.id, filter=','.join(str(i) for i in sources.ids))) else: query = 'SELECT {fields} FROM {model} WHERE {field} IN ({filter})' self._cr.execute( query.format(fields=', '.join(str(i) for i in constraints[0]), model=table, field=field, filter=','.join(str(i) for i in sources.ids))) for row in self._cr.dictfetchall(): filters = get_filters(row, field) self._upd_del(table, field, target, filters, row.get(field)) def _check_references(self, sources, target): '''Get all reference field and send to update it. ''' query = """ SELECT name, model FROM ir_model_fields WHERE ttype = 'reference' AND model!=%(model)s;""" query_args = dict(model=sources._name) self._cr.execute(query, query_args) refks = self._cr.fetchall() for field_name, model_name in refks: model = self.env.registry(model_name) if not model: continue # noqa if not getattr(model, '_auto', False): continue # noqa if hasattr(model, '_fields'): field = model._fields.get(field_name, False) if not field or not field.store: continue # noqa if hasattr(model, '_table'): table = model._table else: table = model_name.replace('.', '_') self._upd_reference(table=table, field=field_name, sources=sources, target=target, model=model_name) self._check_informal_reference(sources, target) def _upd_reference(self, table, field, sources, target, model_field=None, model=None): '''Update reference (field) to destination value (dst_id) where actual field value are in merging object ids (src_ids). if not constraint involve the field to update. Update all matching rows else check one by one if can update and update it or delete. ''' constraints = self._get_contraints(table, field) if model_field: SUBQUERY = ["{field} = {id}", "{model_field}='{model}'"] else: SUBQUERY = ["{field} = '{model},{id}'"] if not constraints: if model_field: query = """UPDATE {table} SET {field}={target} WHERE {field} IN ({filter}) AND {model_field}='{model}';""" self._cr.execute( query.format(table=table, field=field, target=target.id, filter=','.join(str(i) for i in sources.ids), model_field=model_field, model=model)) return for record in sources: query = ("UPDATE {table} " "SET {field}='{model},{target}' " + "WHERE " + SUBQUERY[0]) self._cr.execute( query.format(table=table, field=field, model=model, target=target.id, id=record.id)) return else: query = ("SELECT {fields} FROM {table} WHERE " + ' AND '.join(SUBQUERY)) for sources in sources: _query = (query.format(fields=', '.join( str(i) for i in constraints[0]), table=table, field=field, model=model, id=sources.id, model_field=model_field)) self._cr.execute(_query) for row in self._cr.dictfetchall(): filters = get_filters(row, field) self._upd_del(table, field, target, filters, sources.id, model_field) def _check_informal_reference(self, sources, target): for ref in self.env['informal.reference'].sudo().search([]): if self._check_field_exist( ref.table_name, (ref.id_field_name, ref.model_field_name)): model = sources._name if ref.model_field_value == IS_MODEL_ID: args = [('model', '=', model)] model_id = IrModel.sudo().search(args) if model_id: self._upd_reference(table=ref.table_name, field=ref.id_field_name, sources=sources, target=target, model_field=ref.model_field_name, model=str(model_id.id)) else: self._upd_reference(table=ref.table_name, field=ref.id_field_name, sources=sources, target=target, model_field=ref.model_field_name, model=model) def _check_field_exist(self, table_name, column_names): query = """ SELECT column_name FROM information_schema.columns WHERE table_schema='public' AND table_name=%s AND column_name=%s """ for column_name in column_names: self._cr.execute(query, (table_name, column_name)) if not self._cr.rowcount: return False return True
class WorkDistributionModel(models.Model): _name = WORKDIST_MODELNAME model = fields.Many2one( 'ir.model', required=True, help='Model where Work Distribution Strategies will be applied.') model_name = fields.Char(related='model.model', readonly=True) group_field = fields.Many2one( 'ir.model.fields', 'Group Field', help='many2one field where that it value determine domain of ' 'distribution destination.') strategy_by_group = fields.Boolean() use_domain_builder = fields.Boolean() domain = fields.Text( help='Odoo domain to search in destination_field`s model.', default=lambda self: _(DOMAIN_DEFAULT)) build_domain = fields.Char( help='Odoo domain to search in destination_field`s model.', string='Domain') destination_field = fields.Many2one( 'ir.model.fields', 'Destination Field', required=True, help='Field that it value it will determinate by distribution ' 'strategy apply.') destination_model = fields.Char(related='destination_field.relation', readonly=True) other_fields = fields.Text( 'Other fields', help='Python Dictionary with others variables (Keys) ' 'used on some distribution strategy and ' 'model\' fields where it value are. \n ' 'Eg: {"date": "date_from"}', default='{}') strategy_ids = fields.Many2many( 'work.distribution.strategy', 'distribution_model_strategy', 'model_id', 'strategy_id', 'Strategies', required=True, help='Work Distribution Strategies possible to apply on this model ' 'objects.') action = fields.Many2one('ir.actions.act_window') strategy_field = fields.Many2one('ir.model.fields', 'Setting Field') when_apply = fields.Selection( [('all', 'Always'), ('no_set', 'When no value set')], default='all') effort_domain = fields.Text(help="Odoo domain to use on effort " "based strategies. \nEg: " "[('state','not in',('done','cancel')]") translations = fields.Many2many('ir.translation') @api.onchange('use_domain_builder') def onchange_use_domain_builder(self): ''' If the domain constructor changes it assigns default values to the domain. By default use_domain_builder is 'True'. ''' if self.use_domain_builder: self.build_domain = '' self.domain = '' else: self.domain = (self.build_domain or '') + _(DOMAIN_DEFAULT) @api.constrains('other_fields') def _check_other_fields(self): ''' Validates that the field names defined in the 'other_field' dictionary are among the names of the fields defined for the selected strategy. And that the defined fields are in some existing model. ''' for model in self: fields_name = model.strategy_ids.get_fields_name() if fields_name: other_fields = safe_eval(model.other_fields or '{}') if not other_fields or not all( other_fields.get(f, False) for f in fields_name): raise ValidationError( _('Other fields must define all fields used on ' 'selected strategies.')) obj = self.env[model.model.model] for field in other_fields.itervalues(): if not obj or not obj._fields.get(field, False): raise ValidationError( _('Some Other fields defined not exist on ' 'destination model')) return True @api.constrains('strategy_ids') def _check_strategy_ids(self): '''Validate that when no group field is defined, only one strategy can be selected. ''' for model in self: if not model.group_field and len(model.strategy_ids) > 1: raise ValidationError( _('When no group field is defined only one strategy ' 'must be selected.')) return True @api.model @api.onchange('strategy_ids') def onchange_strategy_ids(self): ''' When the selected strategies change, it is validated that the field 'Other field' maintains the structure of a dictionary that its values are valid. ''' warning = False try: other_fields = safe_eval(self.other_fields or '{}') except (ValueError, SyntaxError, ): other_fields = {} warning = { 'title': _('Warning!'), 'message': _('Other fields must be a python dict.') } fields_name = self.strategy_ids.get_fields_name() other_fields.update({n: n for n in fields_name - set(other_fields)}) self.other_fields = str(other_fields) return {'warning': warning} if warning else None @api.model @api.onchange('strategy_by_group') def onchange_strategy_by_group(self): ''' If the field 'Strategy by group' is not selected the field 'Group field' is assigned a false value and is hidden. ''' if not self.strategy_by_group: self.group_field = False def applicable(self, values): if not self.when_apply or self.when_apply == 'all': return True else: return not values.get(self.destination_field.name, False) @api.model def unlink_rest(self): self.search([('id', 'not in', self.ids)]).unlink() @api.model @api.returns('self', lambda value: value.id) def create(self, values): if values.get('group_field', False): self.sudo().create_related(values) res = super(WorkDistributionModel, self).create(values) res.update_translations() return res def create_related(self, values): """ Create action and field associated. """ field_obj = self.env['ir.model.fields'] destination_field = field_obj.browse(values['destination_field']) group_field = field_obj.browse(values['group_field']) model_id = values.get('model', False) model = self.env['ir.model'].browse(model_id) strategy_field = self.create_field(group_field.relation, model, destination_field) action = self.create_actions( group_field.relation, model.name, destination_field.field_description, strategy_field) values.update(dict(strategy_field=strategy_field, action=action)) def create_field(self, group_model, model, destination_field): ''' Create field to save which distribution strategy use on each group_field model objects. ''' field_obj = self.env['ir.model.fields'] model_obj = self.env['ir.model'] group_model = model_obj.search([('model', '=', group_model)]) field_base_name = '%s_%s' % (model.model.replace('.', '_'), destination_field.name) field_name = 'x_%s_id' % field_base_name field_data = { 'model': group_model[0].model, 'model_id': group_model.ids[0], 'name': field_name, 'relation': 'work.distribution.strategy', 'field_description': _("Work Distribution Strategy for %s on %s") % (destination_field.field_description, model.name), 'state': 'manual', 'ttype': 'many2one' } new_field = field_obj.create(field_data) self.create_ir_model_data_reference('ir.model.fields', new_field.id, field_name) return new_field.id def create_actions(self, group_model, model_name, destination_field, strategy_field_id): ''' Create actions to config with strategy use on each group_field model objects. ''' action_obj = self.env['ir.actions.act_window'] value_obj = self.env['ir.values'] name = (_("Define Work Distribution Strategy for '%s' on '%s'") % (destination_field, model_name)) rol = self.env.ref('xopgi_work_distributor.group_distributor_manager', raise_if_not_found=False) new_act = action_obj.create({ 'name': name, 'type': 'ir.actions.act_window', 'res_model': WIZARD_NAME, 'src_model': group_model, 'view_type': 'form', 'view_mode': 'form', 'target': 'new', 'groups_id': LINK_RELATED(rol.id) if rol else False, 'context': { 'strategy_field_id': strategy_field_id, 'field_to_show': FIELD_NAME_TO_SHOW_ON_WIZARD(group_model) } }) self.create_ir_model_data_reference('ir.actions.act_window', new_act.id, name) new_val = value_obj.create({ 'name': name, 'model': group_model, 'key2': 'client_action_multi', 'value': "ir.actions.act_window," + str(new_act.id) }) self.create_ir_model_data_reference('ir.values', new_val.id, name) return new_act.id def create_ir_model_data_reference(self, model, res_id, name): '''Create ir.model.data entry for each field, action or value created on run time, to ensure it be removed at module uninstall. ''' self.env['ir.model.data'].create({ 'model': model, 'res_id': res_id, 'module': 'xopgi_work_distributor', 'name': '%s-%s' % (model, name), 'noupdate': True }) def unlink(self): """ Unlink action and field associated. """ actions_to_unlink = [i.action.id for i in self if i.action] fields_to_unlink = [i.strategy_field.id for i in self if i.strategy_field] self.update_translations(force_delete=True) result = super(WorkDistributionModel, self).unlink() self.sudo().unlink_actions(actions_to_unlink) self.sudo().unlink_fields(fields_to_unlink) return result def unlink_actions(self, actions_to_unlink): '''Remove actions of distributions model unlinked. ''' if not actions_to_unlink: return model = 'ir.actions.act_window' self.unlink_ir_model_data_reference(model, actions_to_unlink) self.env[model].browse(actions_to_unlink).unlink() for action_id in actions_to_unlink: args = [('value', '=', "%s,%d" % (model, action_id))] values = self.env['ir.values'].search(args) if values: self.unlink_ir_model_data_reference('ir.values', values.ids) values.unlink() def unlink_fields(self, fields_to_unlink): '''Remove fields of distributions model unlinked. ''' if not fields_to_unlink: return model = 'ir.model.fields' self.unlink_ir_model_data_reference(model, fields_to_unlink) self.env[model].browse(fields_to_unlink).unlink() def unlink_ir_model_data_reference(self, model, ids): self.env['ir.model.data'].search([ ('model', '=', model), ('res_id', 'in', ids), ('module', '=', 'xopgi_work_distributor') ]).unlink() @api.multi def write(self, values): temp_fields = ['destination_field', 'group_field', 'model'] if any(f in values for f in temp_fields): actions_to_unlink = [i.action.id for i in self if i.action] fields_to_unlink = [i.strategy_field.id for i in self if i.strategy_field] self.sudo().unlink_actions(actions_to_unlink) self.sudo().unlink_fields(fields_to_unlink) for item in self.sudo(): if item.translations: item.translations.unlink() result = super(WorkDistributionModel, self).write(values) group_field = values.get('group_field', False) if any(values.get(f, False) for f in temp_fields): for item in self: temp_fields = ['destination_field', 'group_field', 'model'] vals = {f: getattr(item, f).id for f in temp_fields} self.create_related(vals) for f in temp_fields: vals.pop(f, None) item.write(vals) if any(f in values for f in temp_fields): self.update_translations(force_create=group_field) return result @api.multi def update_translations(self, force_create=False, force_delete=False): to_unlink = self.env['ir.translation'] for item in self: if not force_delete and item.group_field: if force_create or not item.translations: translations = self.create_translations( item.group_field.relation, item.strategy_field.name, item.strategy_field.field_description, type='field') translations |= self.create_translations( 'ir.actions.act_window', 'name', item.action.name, res_id=item.action.id) item.write(dict( translations=[REPLACEWITH_RELATED(*translations.ids)] )) if item.translations and (force_delete or not item.group_field): to_unlink |= item.translations if to_unlink: to_unlink.unlink() def create_translations(self, model, field, src, res_id=0, type='model'): result = self.env['ir.translation'] for lang in self.env['res.lang'].search([]): result |= result.create({ 'type': type, 'res_id': res_id, 'module': 'xopgi_work_distributor', 'name': '%s,%s' % (model, field), 'src': src, 'value': src, 'lang': lang.code }) return result
class FieldMergeWay(models.Model): _name = 'field.merge.way' name = fields.Char(required=True, translate=True) code = fields.Selection( [('add', 'Add both fields'), ('min', 'Keep the minimal'), ('max', 'Keep the maximal'), ('average', 'Find the average'), ('sum', 'Find the sum')], required=True, ) _sql_constraints = [('name_unique', 'unique (name)', 'Strategy must be unique!')] def apply(self, sources, target, field): method = getattr(self, 'apply_' + self.code, None) if method: return method(sources, target, field) else: return None def apply_add(self, sources, target, field): result = getattr(target, field.name, None) if field.ttype == 'char': union = ' ' elif field.ttype == 'text': union = ' ' else: union = False for obj in sources: temp = getattr(obj, field.name, None) if result and temp: result += union + temp if union else temp elif not result: result = temp return result def apply_average(self, sources, target, field): result = getattr(target, field.name, 0) count = 1 if result not in [False, None] else 0 for obj in sources: temp = getattr(obj, field.name, None) if not any(var in [False, None] for var in (result, temp)): result += temp count += 1 return result / count if result and count else 0 def apply_sum(self, sources, target, field): return self._reduce(operator.add, sources, target, field, 0) def apply_max(self, sources, target, field): from xoutil.infinity import Infinity return self._reduce(max, sources, target, field, -Infinity) def apply_min(self, sources, target, field): from xoutil.infinity import Infinity return self._reduce(min, sources, target, field, -Infinity) def _reduce(self, f, sources, target, field, initial): result = getattr(target, field.name, initial) for temp in sources.mapped(field.name): result = f(result, temp) return result
class BounceRecord(models.Model): '''An index for bounce address to message, thread and recipient. This model encodes the same information of old VERP addresses but allow a simpler address, like: ``[email protected]``. ''' _name = 'xopgi.verp.record' bounce_alias = fields.Char( help=('The alias where the bounce was sent to. You may change the' 'alias configuration midways and this will still work'), required=True, default='bounces' ) thread_index = fields.Char( help='The unique index reference for the thread.', required=True, index=True, ) message_id = fields.Many2one( 'mail.message', required=True, # ondelete=cascade: If the message is delete remove the VERP # address. This happens for invitations, for instance. The # message is create and the bounce address is properly generated, # but afterwards the message is removed. This make the bounce # reference ephemeral for these cases, but if the message is lost # we won't be able to know who to notify. ondelete="cascade", help=('The message id originating this notification. This allows ' 'to know who to notify about bounces.') ) reference = fields.Char( help='The unique part for the bounce address.', size=100, required=True, index=True, ) recipient = fields.Char( help='The recipient for which this VERP address was created', required=True, index=True, ) _sql_constraints = [ ('verp_unique', 'unique (reference)', 'The VERP is duplicated.'), ] @api.model @api.returns('self', lambda r: r.reference) # return the reference def create(self, vals): try: from openerp.addons.mail.xopgi.index import generate_reference except ImportError: try: # Odoo 9 from openerp.addons.mail.models.xopgi.index import generate_reference except ImportError: # Odoo 10 from odoo.addons.mail.models.xopgi.index import generate_reference reference = generate_reference( lambda r: self.search([('reference', '=', r)]), start=3, lower=True ) assert reference vals.update(reference=reference) return super(BounceRecord, self).create(vals) @api.model def cleanup(self): '''Remove all bounce address references that are too old. This should be called in a cron task. ''' self._cr.execute(''' WITH aged AS ( SELECT id, ((NOW() at time zone 'UTC') - create_date) AS age FROM xopgi_verp_record ) SELECT id FROM aged WHERE age >= %s ''', ('10 days', )) elders = [row[0] for row in self._cr.fetchall()] if elders: self.sudo().browse(elders).unlink()
class MergePartnerGroup(models.Model): """A group of partner which are deemed duplicates. - Is a partner when `parent_id` points to another instance of the same type representing the group. - Is a group when has no `parent_id` and several partners point to here. In this case the referenced partner is the destination partner. """ _name = 'xopgi.partner.merge.group' _order = "name asc" dest_partner_id = fields.Many2one( 'res.partner', string='Destination partner' ) partner_ids = fields.Many2many( comodel_name='res.partner', relation='xopgi_partner_merge_group_partners', column1='category_id', column2='partner_id', string='Partners' ) name = fields.Char( related=('dest_partner_id', 'name'), string='Name', readonly=True, store=True, ) @api.multi @mute_logger('openerp.osv.expression', 'openerp.models') def merge(self): """Merge several `partners` into a single destination partner. Original `partners` will be removed from the DB afterwards. Only target will remain. All references to the original partners will be re-establish to the target partner. If `partners` constains less that 2 partners, do nothing. All partners must have the same email. If sources `partner` is none, the target partner defaults to the last created record in `partners`. :param sources: The source partners. :type sources: A recordset of 'res.partners'. :param target: The target partner. :type target: A singleton recordset of 'res.partners' or None. """ sources = self.partner_ids target = self.dest_partner_id if sources.sudo().exists() and len(sources) < 2: raise UserError(_("Constains less that 2 partners, do nothing")) partner_different_emails = { p.email for p in sources if p.email and p.email.strip() } if len(partner_different_emails) > 1: user = self.env.user if user.has_group('xopgi_partner_merge.base_parter_merger'): object_merger = self.env['object.merger'] object_merger.merge(sources, target) else: raise UserError( _("All contacts must have the same email. Only the " "users with Partner Merge rights can merge contacts " "with different emails.") ) object_merger = self.env['object.merger'] object_merger.merge(sources, target) self.unlink()
class MessageRecipients(models.Model): _name = get_modelname(Message) _inherit = _name recipients = fields.Char()
class ControlVariable(models.Model): '''The `control variables` define basic mostly numeric evaluations of a single basic indicator. For instance, you may define 'the total amount of liquidity' you have available. ''' _name = 'cdr.control.variable' _inherits = {'cdr.identifier': 'identifier_id'} identifier_id = fields.Many2one( 'cdr.identifier', required=True, ondelete='cascade', help='Identifier the control variable: name, value, evaluations' ) description = fields.Char( translate=True, help='Description of control variable' ) template = fields.Many2one( 'cdr.control.variable.template', required=True, ondelete='restrict', help='The template is the key for get a value in a control variable' ) args_need = fields.Boolean( related='template.args_need' ) args = fields.Text( help="Python dictionary with arguments that template expect." ) @fields.Property def arguments(self): return evaluate(self.args) if self.args else {} evidences = fields.Many2many( 'cdr.evidence', 'evidence_control_variable_rel', 'var_id', 'evidence_id', ondelete='restrict', help='Are predicates over several control variables' ) active = fields.Boolean( default=True ) cycle = fields.Many2one( 'cdr.evaluation.cycle', help='The control variable are evaluate in evaluation cycle' ) @api.onchange('template', 'template', 'args_need') def onchange_template(self): '''Take values of ''' if self.template and self.args_need: args = [ "\n\t'%s': " % arg for _, arg, _, _ in self.template.definition._formatter_parser() if arg ] if self.args_need else [] self.args = "{%s\n}" % ",".join(args) else: self.args = "" def get_value(self): '''Get name: value dictionary ''' return {v.name: v.result for v in self} def _value(self): '''If value: Verify that the value of control variable is evaluated as python code else return None. ''' import warnings warnings.warn('The _value() method of control variables is ' 'deprecated. Use the `result` property.', stacklevel=2) return self.result @api.requires_singleton def _evaluate(self, now=None): '''Allow to make python expression for 'args' in the template. The field 'args' represent a string of type text. ''' logger.debug('Evaluating %r', self.name) from xoeuf.tools import normalize_datetime result = self.template.eval( normalize_datetime(now or fields.Datetime.now()), self.arguments ) logger.debug('Evaluated %r', self.name) return result @api.constrains('template', 'args') def _check_definition(self): '''Check the control variable definition. ''' for variable in self: try: variable.template.compile(variable.arguments) except Exception as e: raise exceptions.ValidationError( _("Wrong definition: %s") % e.message ) def evaluate(self, cycle): '''Evaluate the control variables in a evaluation cycle. ''' if isinstance(cycle, int): cycle = self.env['cdr.evaluation.cycle'].browse(cycle) import psycopg2 logger.debug('Start evaluation of %r, cycle: %r', self.mapped('name'), cycle) from celery.exceptions import SoftTimeLimitExceeded for var in self: try: value = var._evaluate(cycle.create_date) except SoftTimeLimitExceeded: raise except (psycopg2.InterfaceError, psycopg2.InternalError): # This means the cursor is unusable, so there's no point in # trying to do anything else with it. raise except Exception: logger.exception( 'Error evaluating control variable %s.', var.name ) else: var.write(dict( result=value, cycle=cycle.id, evaluations=[CREATE_RELATED(result=value, cycle=cycle.id)] )) logger.debug('Done computing variable %r', self.mapped('name')) @api.model @api.returns('self', lambda value: value.id) def create(self, vals): logger.debug('Creating variable %r', vals) res = super(ControlVariable, self).create(vals) logger.debug('Created variable %r', res.name) # evaluate by first time to get init value. self.env['cdr.evaluation.cycle'].create_and_evaluate(variables=res) logger.debug('Evaluated variable %r', res.name) return res
class SystemEvent(models.Model): '''Generic CDR event definition. For specifics CDR events implementations just need to inherit by delegation of this model and override evaluate method. Recommendation: define _description for specific event implementations to allow correct user identification. ''' _name = 'cdr.system.event' _description = "Generic CDR event" _order = 'priority' name = fields.Char(translate=True) definition = fields.Char( required=True, help=("Boolean expression combining evidences and operators. " "For example: evidence1 or evidence2 and evidence3")) next_call = fields.Datetime( default=fields.Datetime.now(), help=("The date and time at which this event will be checked again. " "This is when the evidences and its variables will be computed " "again.")) priority = fields.Integer( default=10, help=("Priority in which events are to be evaluated in an evaluation " "cycle. When you run a search they will come sorted according " "to your priority.")) active = fields.Boolean(default=True) evidences = fields.Many2many('cdr.evidence', 'event_evidence_rel', 'event_id', 'evidence_id', compute='get_evidences', store=True) specific_event = fields.Reference( [], compute='_get_specific_event', help='Reference at specific event: basic or recurrent event') state = fields.Selection(EVENT_STATES, help='State of the event: Raising or Not raising') action = fields.Selection(EVENT_ACTIONS_SELECTION, string="Last action") def get_specific_event_models(self): '''Get models that look like specific event. ''' result = [] for model_name in self.env.registry.keys(): model = self.env[model_name] if 'cdr.system.event' in getattr(model, '_inherits', {}): result.append(model) return result def _get_specific_event(self): '''Get specific event reference from it's generic one. ''' for event in self: for model in self.get_specific_event_models(): field = model._inherits.get('cdr.system.event') result = model.search([(field, '=', event.id)], limit=1) if result: event.specific_event = "%s,%d" % (result._name, result.id) @api.depends('definition') @api.onchange('definition') def get_evidences(self): '''Get evidences referenced on event definition. ''' evidences = self.env['cdr.evidence'] for event in self: if event.active and event.definition: domain = [('name', 'in', tuple(get_free_names(event.definition)))] event.evidences = evidences.search(domain) else: event.evidences = evidences def _get_evidences_to_evaluate(self): return self.mapped('evidences') def _evaluate(self): return evaluate(self.definition, **self.evidences.get_bool_value()) @api.constrains('definition') def _check_definition(self): for record in self: try: record._evaluate() except Exception as e: raise exceptions.ValidationError( _("Wrong definition: %s") % e.message) def evaluate(self, cycle): if isinstance(cycle, int): cycle = self.env['cdr.evaluation.cycle'].browse(cycle) for event in self: # call specific event evaluate method to get each # corresponding behavior. event.specific_event.evaluate(cycle) for signalname, signal in EVENT_SIGNALS.items(): # Do not use groupby because a recordset is needed on signaling # send. sender = self.filtered(lambda event: event.action == signalname) if sender: logger.debug('Sending signal (%s) for Events: (%s)', signalname, ', '.join(e.name for e in sender)) signal.send(sender=sender)
class RecurrentRuleDefinition(models.AbstractModel): '''A recurrence definition mixin. The model behind this definition is that of the python module `dateutil.rrule`:mod:. ''' _name = RECURRENT_DESCRIPTION_MODEL _inherit = ['recurrence.abs.day_of_week'] date_from = fields.Date( 'Initial date', required=True, default=fields.Date.today, # XXX: When we return an occurrence this matches the date of the # occurrence. help='Date at which this recurrent event starts.') duration = fields.Integer('Duration', default=1, help='Duration (days) of each occurrence') allday = fields.Boolean( 'All Day', default=True, help='Is this a day-long occurrence?', ) freq = fields.Selection( FREQ, 'Frequency type', default=DEFAULT_FREQ, # XXX: Don't put this, because there's an error in the JS client that # messes up with the invisible toggling we have there. # help='Frecuency type (Daily/Weekly/Monthly/Yearly)' ) interval = fields.Integer( 'Repeat Every', default=DEFAULT_INTERVAL, help='Repeat every ...', ) # Recurrence by month data. days_option = fields.Selection( SELECT_WEEKDAY_MONTHDAY, 'Option', default=DEFAULT_WEEK_MONTH, help='Does this occur on the same day of the month, or the week', ) monthly_day = fields.Integer( 'Days of month', default=lambda *args: datetime.date.today().day) by_week_day = fields.Selection( BYWEEKDAY, 'Reference', default=DEFAULT_BYWEEKDAY, help=("Used in combination with each week day selection. If you " "check Monday, then this can mean: every Monday, the first " "Monday, the last Monday, etc... You may choose several week " "days.")) months = fields.Selection( MONTHS, 'Month', deafault=lambda *args: str(datetime.date.today().month), help="The month of the year at which this event reoccurs.") is_easterly = fields.Boolean( 'By easter', help="For events that reoccurs based on Western Easter Sunday.") byeaster = fields.Integer( 'By easter', default=0, help='Number of days from Western Easter Sunday.') # In this model, the end of recurrence is EITHER: a) does not have and # end, b) after many instances, c) after a given date. end_type = fields.Selection( END_TYPE, 'Recurrence Termination', default=DOES_NOT_END, help="How this recurrence stops from happening.") count = fields.Integer('Repeat', default=5, help='Repeat recurrent event by x times') until = fields.Date('Repeat Until', help='Date end for the recurrent termination') # TODO: Should we need to store this in the DB? rrule = fields.Char( 'RRULE', size=124, readonly=True, default='', compute='_compute_rrule_string', help=( 'This field is update after to creates or to update the recurrent' ' model by the function _update_rrule. By default it is taken' ' value but then it is calculated and takes a similar value.' ' E.g. rrule=FREQ=WEEKLY;INTERVAL=1;BYDAY=TU,MO'), ) @api.depends('count', 'until', 'end_type', 'mo', 'tu', 'we', 'th', 'fr', 'sa', 'su', 'is_easterly', 'byeaster', 'months', 'monthly_day', 'by_week_day', 'freq', 'days_option', 'interval') def _compute_rrule_string(self): for record in self: record.rrule = str(record.get_rrule_from_description()) @api.requires_singleton def get_rrule_from_description(self): '''The recurrent rule that describes this recurrent event. :returns: `dateutil.rrule.rrule`:class:. ''' from xoeuf.tools import normalize_date kwargs = dict( dtstart=normalize_date(self.date_from), interval=max(1, self.interval or 0), ) if self.end_type == ENDS_AT: kwargs['until'] = max(normalize_date(self.date_from), normalize_date(self.until)) # or date_to? elif self.end_type == ENDS_AFTER_MANY: kwargs['count'] = min(1, self.count) else: assert self.end_type == DOES_NOT_END if self.days_option == USE_WEEK_DAY: kwargs['byweekday'] = self.byweekday elif self.days_option == USE_MONTH_DAY: kwargs['bymonthday'] = self.monthly_day else: assert False if self.is_easterly: kwargs['byeaster'] = self.byeaster else: kwargs['bymonth'] = int(self.months) return _rrule.rrule(FREQ_TO_RULE[self.freq], **kwargs) @fields.Property def byweekday(self): return self._weekno @api.constrains('monthly_day') def _check_monthly_day_no_longer_than_month(self): for record in self: if not (-31 <= record.monthly_day <= 31): raise ValidationError( 'You must provide a valid day of the month') @api.constrains('byeaster', 'is_easterly') def _check_byeaster_no_longer_than_year(self): for record in self: if record.is_easterly: if not (-365 <= record.byeaster <= 365): raise ValidationError( 'By Easter must not extend longer than a year') @api.requires_singleton def iter_from(self, start=None): '''Return an iterator that yields each occurrence after `start` date. If `start` is None, start at the first `date` (field ``date_from``). :returns: A generator of the dates of the occurrences. .. warning:: This can be an infinite iterator. ''' from xoeuf.tools import normalize_date if start is None: start = normalize_date(self.date_from) return self.get_rrule_from_description().xafter(start)
class WorkDistributionStrategy(models.Model): _name = 'work.distribution.strategy' name = fields.Char(required=True, translate=True) predefine = fields.Boolean() code = fields.Text(required=True, default=''' # Python code to return on result var value to set on # destination field or None to no update it. # E.g: result = False # self, dist_model, values, candidates and **kwargs are able to use: # self => active strategy (on new api). # dist_model => active model name. # values => python dict to passed to create method. # **kwargs => python dict set on field ``other fields``. ''') other_fields = fields.Char( help='Python List with others variables' 'needed on this distribution strategy.\n ' 'Eg: ["date", "qtty"]', default='[]') _sql_constraints = [ ('name_unique', 'unique (name)', 'Strategy must be unique!') ] def apply(self, dist_model, values, **kwargs): method = (getattr(self, self.code, None) if self.predefine else self.custom) candidates = _evaluate_domain(dist_model, values) if method and candidates: return method(dist_model, candidates, values, **kwargs) def uniform(self, dist_model, candidates, values, **kwargs): """Detect the next corresponding domain id and update values with it. """ table = self.env[dist_model.model.model]._table if dist_model.group_field: query = ("SELECT %s FROM %s WHERE %s = %%s ORDER BY id DESC" % (dist_model.destination_field.name, table, dist_model.group_field.name)) params = (values.get(dist_model.group_field.name, False),) else: query = ("SELECT %s FROM %s WHERE %s in %%s ORDER BY id DESC" % (dist_model.destination_field.name, table, dist_model.destination_field.name)) params = (tuple(candidates.ids),) self.env.cr.execute(query, params=params) last_dist = self.env.cr.fetchone() last_dist = last_dist[0] if last_dist and last_dist[0] else 0 candidates = candidates.sorted(key=lambda r: r.id) return next( (id for id in candidates.ids if not last_dist or id > last_dist), candidates.ids[0] ) def effort(self, dist_model, candidates, values, **kwargs): return self._effort(dist_model, candidates, values) def _effort_commons(self, dist_model, **kwargs): model = self.env[dist_model.model.model] today = normalize_datetime(fields.Date.context_today(self)) date_field = model._fields[kwargs.get('date_start')] to_str = date2str if isinstance(date_field, fields.Date) else dt2str return model, today, date_field, to_str def effort_month(self, dist_model, candidates, values, **kwargs): model, low_date, date_field, to_str = self._effort_commons(dist_model, **kwargs) DAYS = 30 upp_date = low_date + timedelta(DAYS) strlow_date = to_str(low_date) strupp_date = to_str(upp_date) return self._effort( dist_model, candidates, values, date_field=date_field.name, date_start=strlow_date, date_end=strupp_date) def around_effort(self, dist_model, candidates, values, **kwargs): model, today, date_field, to_str = self._effort_commons(dist_model, **kwargs) DAYS = 7 TOTAL_DAYS = DAYS * 2 + 1 item_date = values.get(kwargs.get('date_start'), False) item_date = normalize_datetime(item_date) if item_date else today low_date = (today if item_date < today + timedelta(DAYS) else item_date - timedelta(DAYS)) upp_date = low_date + timedelta(TOTAL_DAYS) strlow_date = to_str(low_date) strupp_date = to_str(upp_date) return self._effort( dist_model, candidates, values, date_field=date_field.name, date_start=strlow_date, date_end=strupp_date ) def future_effort(self, dist_model, candidates, values, **kwargs): model, today, date_field, to_str = self._effort_commons(dist_model, **kwargs) return self._effort( dist_model, candidates, values, date_field=date_field.name, date_start=to_str(today) ) def _effort(self, dist_model, candidates, values, date_field=False, date_start=False, date_end=False): model = self.env[dist_model.model.model] min_value = None next_dist = False group_field_name = (dist_model.group_field.name if dist_model.group_field else False) args = ([(group_field_name, '=', values[group_field_name])] if group_field_name else []) args.extend(safe_eval(dist_model.effort_domain) if dist_model.effort_domain else []) if date_field: if date_start: args.append((date_field, '>=', date_start)) if date_end: args.append((date_field, '<=', date_end)) for x in candidates.ids: current_effort = model.search_count( args + [(dist_model.destination_field.name, '=', x)]) if min_value is None or min_value > current_effort: min_value = current_effort next_dist = x return next_dist def custom(self, dist_model, candidates, values, **kwargs): if self.code.strip().startswith('{'): return safe_eval(self.code or '{') else: local_dict = locals() local_dict.update(globals().get('__builtins__', {})) safe_eval(self.code, local_dict, mode='exec', nocopy=True) return local_dict.get('result', None) @api.model def get_fields_name(self, mixed=True): result = [] method = getattr(result, 'extend' if mixed else 'append') for strategy in self: method(safe_eval(strategy.other_fields) if strategy.other_fields else []) return set(result) if mixed else result
class WorkDistributorWizard(models.TransientModel): _name = WIZARD_NAME name = fields.Char() strategy_id = fields.Many2one( 'work.distribution.strategy', 'Strategy', help='Strategy of work distribution to apply to this items.') info = fields.Text() @api.model def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): result = super(WorkDistributorWizard, self).fields_view_get( view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) if view_type != 'form' or not result.get('fields', {}).get( 'strategy_id', False): return result active_model = self.env.context.get('active_model', False) strategy_field = self.env.context.get('strategy_field_id', False) model = self.env[WORKDIST_MODELNAME].search( [ ('group_field.relation', '=', active_model), ('strategy_field', '=', strategy_field) ], limit=1 ) result['fields']['strategy_id']['domain'] = [ ('id', 'in', model.strategy_ids.ids)] return result @api.model def default_get(self, fields_list): values = super(WorkDistributorWizard, self).default_get(fields_list) if 'info' in fields_list: active_ids = self.env.context.get('active_ids', False) active_model = self.env.context.get('active_model', False) ir_model = self.env['ir.model'] model = ir_model.search([('model', '=', active_model)])[0] field = self.env['ir.model.fields'].browse( self.env.context.get('strategy_field_id')) names = [_('%s\n Strategy: %s') % (item.name_get()[0][1], getattr(item, field.name).name or '') for item in self.env[active_model].browse(active_ids)] info = _("%s(s) to set work distribution strategy:\n" " * %s") % (model.name, '\n * '.join(names)) values.update({'info': info}) return values @api.guess def action_config(self, *args, **Kargs): return {'type': 'ir.actions.act_window_close'} @api.model @api.returns('self', lambda value: value.id) def create(self, vals): res = super(WorkDistributorWizard, self).create(vals) active_ids = self.env.context.get('active_ids', False) active_model = self.env.context.get('active_model', False) field = self.env['ir.model.fields'].browse( self.env.context.get('strategy_field_id')) if not active_model: raise Warning(_('Configuration Error!'), _('The is no active model defined!')) self.env[active_model].browse(active_ids).write( {field.name: res.strategy_id.id}) return res
class AnalyticAccount(models.Model): _inherit = get_modelname(AccountAnalyticAccount) code = fields.Char(default=lambda self: self.env['ir.sequence'].get( 'account.analytic.account'))
class AliasMockerMixin(models.AbstractModel): '''A mixin that mocks mail.alias. True implementations will always read the data from the 'mail.alias' model. A virtual mail alias is simply made available so that other parts of the systems (views, most importantly) are easily made. A virtual alias is by all means a 'mail.alias' that gains other attributes via the 'alias_defaults' dictionary. Proposed usage:: >>> class project_valias(models.Model): ... _auto = False ... _name = 'project.valias' ... _inherit = ['xopgi.mail.alias.mocker'] ... ... project_id = fields.Many2one('project.project', string='Project') Then the 'project_id' attribute of 'project.valias' will be the key 'project_id' in the 'alias_default' attribute of any 'mail_alias'. ''' _name = 'xopgi.mail.alias.mocker' alias_name = fields.Char( 'Alias', required=True, help=("The name of the email alias, e.g. 'jobs' if " "you want to catch emails " "for <*****@*****.**>")) alias_domain = fields.Char( 'Domain', required=True, default=get_default_alias_domain, help=("The domain of the email alias, e.g. " "'example.my.openerp.com' if you want to catch emails " "for <*****@*****.**>")) alias_defaults = fields.Text( 'Default Values', default='{}', help=("A Python dictionary that will be evaluated to " "provide default values when creating new " "records for this alias.")) alias_force_thread_id = fields.Integer( 'Record Thread ID', help=("Optional ID of a thread (record) to which " "all incoming messages will be attached, " "even if they did not reply to it. If set, " "this will disable the creation of new " "records completely.")) @api.multi def read(self, fields=None, load='_classic_read'): """Read the ids from mail.alias if any of fields are not present on mail.alias model then search it on alias_defaults dict. """ Model = self.env['mail.alias'] extraneous = [] for field in fields: if field not in Model._fields.keys(): extraneous.append(field) if extraneous: fields = list(set(fields) - set(extraneous)) if extraneous and 'alias_defaults' not in fields: default_added = True fields.append('alias_defaults') else: default_added = False result = Model.browse(self.ids).read(fields) if not extraneous: return result else: for row in result: defaults = str2dict(row['alias_defaults']) for field in extraneous: row[field] = defaults.pop(field, False) # Restore the defaults but it will have only the keys not # in 'extraneous' (those upgraded to fields) if not default_added: row['alias_defaults'] = repr(defaults) else: row.pop('alias_defaults') return result @api.model def _parse_fields(self, alias_id, vals): '''The fields not present on mail.alias model are include on alias_defaults dictionary. ''' Aliases = self.env['mail.alias'] extraneous = [] fields = set(vals.keys()) for field in vals.keys(): if field not in Aliases._fields.keys(): extraneous.append(field) if extraneous: fields -= set(extraneous) defaults = {} if 'alias_defaults' in fields: defaults = str2dict(vals['alias_defaults'], 'Default Values') else: if alias_id: record = Aliases.browse(alias_id) row = record.read(['alias_defaults']) defaults = str2dict(row[0]['alias_defaults'], 'Default Values') for field in extraneous: value = vals.pop(field, False) if field in defaults: defaults.update({field: value}) else: defaults.setdefault(field, value) vals['alias_defaults'] = repr(defaults) return vals
class Message(models.Model): _inherit = 'mail.message' # Make the email_from create an index, the 'search' in the router is slow # without it. email_from = fields.Char(index=True)