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 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 RecurrentEvent(models.Model): _name = 'cdr.recurrent.event' _description = "Recurrent CDR event" _inherits = { 'cdr.system.event': 'event_id', 'cdr.recurrent.event.def': 'recurrence_def' } event_id = fields.Many2one('cdr.system.event', required=True, ondelete='cascade') time = fields.Float() recurrence_def = fields.Many2one('cdr.recurrent.event.def', required=True, ondelete='cascade') def update_event(self, value): '''Update the fields next call, state and action for an event. When an event is evaluated is necessary to update its values. ''' next_call = self.recurrence_def.next_date(self.recurrence_def.rrule) state = 'raising' if value else 'not_raising' action = 'raise' if state == 'raising' else 'do_nothing' values = dict(next_call=next_call, state=state, action=action) self.write(values) def evaluate(self, cycle): '''Evaluate the recurrent event in a evaluation cycle. ''' if isinstance(cycle, int): cycle = self.env['cdr.evaluation.cycle'].browse(cycle) try: value = self.event_id._evaluate() except Exception: logger.exception('Error evaluating event %s defined as: ', self.name, self.definition) return None else: self.update_event(value) @api.model @api.returns('self', lambda value: value.id) def create(self, vals): if any(field in self.recurrence_def._fields for field in vals): vals.update(is_recurrent=True) return super(RecurrentEvent, self).create(vals)
class FieldMergeWayRel(models.Model): _name = 'field.merge.way.rel' name = fields.Many2one('ir.model.fields', required=True) merge_way = fields.Many2one('field.merge.way', required=True) model = fields.Many2one('ir.model', required=True) @api.multi def meld(self, sources, target): res = {} for item in self: val = item.merge_way.apply(sources, target, item.name) if val is not None: res[item.name.name] = val return res
class StatementLine(models.Model): _inherit = 'account.bank.statement.line' # In order to be able to efficiently create the statement from the # payments I need to keep the relation of each line which the payment that # generated it. created_from_payment_id = fields.Many2one('account.payment') @api.multi def _reconcile_from_payments(self): self = self.filtered(lambda l: l.created_from_payment_id and l. created_from_payment_id._payment_move_line) for line in self: line.process_reconciliation( payment_aml_rec=line.created_from_payment_id._payment_move_line ) @api.multi def button_cancel_reconciliation(self): # Allow to revert automatic reconciliations of any date. automatically_created = self.filtered( lambda l: l.created_from_payment_id and l.created_from_payment_id. _payment_move_line) res = super( StatementLine, automatically_created.with_context( check_move_validity=False)).button_cancel_reconciliation() other = self - automatically_created if other: res = super(StatementLine, other).button_cancel_reconciliation() return res
class CommonThreadWizard(models.TransientModel): _name = 'common.thread.wizard' model_id = fields.Selection( string='Model', selection=lambda self: get_model_selection(self), required=True) view = fields.Many2one('xopgi.selectable.view') views_count = fields.Integer(compute='_get_views_count') @api.onchange('model_id') @api.depends('model_id') def _get_views_count(self): selectable_view = self.env['xopgi.selectable.view'] for wizard in self: if wizard.model_id: conf_views = selectable_view.get_views(self.model_id) views_count = len(conf_views) self.views_count = views_count if views_count >= 1: self.view = conf_views[0] else: self.views_count = 0 self.view = False def get_thread_action(self, res_id=None): """ Returns the action that shows the form of this model """ return self.view.get_action(model=self.model_id, res_id=res_id)
class crm_valias(models.Model): _name = 'crm.valias' _inherit = ['xopgi.mail.alias.mocker'] type = fields.Selection( [('lead', 'Lead'), ('opportunity', 'Opportunity')], 'Type', index=True, help="Type of object to create by incoming messages.") if MAJOR_ODOO_VERSION < 9: section_id = fields.Many2one(TEAM_MODEL_NAME, string='Sale Team') else: team_id = fields.Many2one(TEAM_MODEL_NAME, string='Sale Team') user_id = fields.Many2one('res.users', string='Team Leader')
class AccountConfigSettings(models.TransientModel): _inherit = "account.config.settings" ugl_journal_id = fields.Many2one(related="company_id.ugl_journal_id") ugl_gain_account_id = fields.Many2one( related="company_id.ugl_gain_account_id") ugl_loss_account_id = fields.Many2one( related="company_id.ugl_loss_account_id") @api.onchange('ugl_journal_id') def _update_ugl_gain_and_loss_account(self): if not self.ugl_gain_account_id: self.ugl_gain_account_id = self.ugl_journal_id.default_credit_account_id if not self.ugl_loss_account_id: self.ugl_loss_account_id = self.ugl_journal_id.default_debit_account_id
class AnalyticAccount(models.Model): _inherit = models.get_modelname(AccountAnalyticAccount) manager_id = fields.Many2one( 'res.users', 'Account Manager', track_visibility='onchange' )
class Partner(models.Model): _inherit = 'res.partner' property_account_payment_advance_id = fields.Many2one( 'account.account', company_dependent=True, string="Pre-payment Account", domain=REGULAR_ACCOUNT_DOMAIN, help=("This account will be used instead of the default one as the" "payment advance account for the current partner"), required=True) property_account_receivable_advance_id = fields.Many2one( 'account.account', company_dependent=True, string="Pre-collection Account", domain=REGULAR_ACCOUNT_DOMAIN, help=("This account will be used instead of the default one as the" "receivable advance account for the current partner"), required=True)
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 XopgiBoardWidgetRel(models.AbstractModel): _name = WIDGET_REL_MODEL_NAME _order = 'priority' widget = fields.Many2one(WIDGET_MODEL_NAME, delegate=True, required=True, ondelete='cascade') priority = fields.Integer(default=1000) def get_widgets(self): """ Get all widget dicts for uid and sorts them by priority. """ models = self.get_widget_capable_models() widgets = sorted(itertools.chain( *[list(model.get_user_widgets()) for model in models]), key=operator.attrgetter('priority')) result = [] # Adding missing widget for widget in widgets: widget.get_set_widgets(result) return result def get_user_widgets(self): """ It must be implemented on extended models. Should return a recordset of user's corresponding widgets. """ raise NotImplementedError() def get_widget_capable_models(self): """ Get a list of models instances that have `get_user_widgets` item """ result = [] for model in self.env.registry.values(): if hasattr(model, "get_user_widgets"): if model._name != WIDGET_REL_MODEL_NAME: result.append(self.env[model._name]) return result def get_set_widgets(self, result): """ Update in-place result adding missing widgets. """ for widget in self.read(fields=[ 'name', 'category', 'template_name', 'xml_template', 'python_code' ]): widget.pop('id', None) if widget not in result: result.append(widget)
class Company(models.Model): _inherit = "res.company" ugl_journal_id = fields.Many2one( 'account.journal', 'Unrealized gain & loss journal', domain=GENERAL_JOURNAL_DOMAIN, ) ugl_gain_account_id = fields.Many2one( 'account.account', 'Unrealized gain account', domain=REGULAR_ACCOUNT_DOMAIN, ) ugl_loss_account_id = fields.Many2one( 'account.account', 'Unrealized loss account', domain=REGULAR_ACCOUNT_DOMAIN, )
class Invoice(models.Model): '''An account invoice. Parent's agency field and use it in search view. ''' _inherit = 'account.invoice' partner_company = fields.Many2one(related='partner_id.parent_id', string="Partner's Company", store=True)
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 project_valias(models.Model): _name = 'project.valias' _inherit = ['xopgi.mail.alias.mocker'] @api.model def _get_models(self): models = _get_model_ids(self) return models.name_get() alias_model_id = fields.Selection( _get_models, 'Aliased Model', required=True, help=("The model (OpenERP Document Kind) to " "which this alias corresponds. Any " "incoming email that does not reply to an " "existing record will cause the creation " "of a new record of this model " "(e.g. a Project Task)")) project_id = fields.Many2one('project.project', string='Project') user_id = fields.Many2one('res.users', string='Project Manager')
class ResGroupWidget(models.Model): _name = 'res.group.widget' _order = 'group, priority' _inherit = WIDGET_REL_MODEL_NAME group = fields.Many2one(get_modelname(res_groups), required=True) def get_user_widgets(self): '''Returns the groups in which the current user is located ''' return self.sudo().search([('group.users', '=', self._uid) ]).sudo(self._uid)
class NewThreadWizard(models.TransientModel): _name = 'new.thread.wizard' _inherit = 'common.thread.wizard' message_id = fields.Many2one('mail.message', 'Message', readonly=True) leave_msg = fields.Boolean( 'Preserve original message', default=True, help="Check for no remove message from original thread.") @api.model def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): context = dict(self._context or {}) if self._uid != SUPERUSER_ID and not self.env['res.users'].has_group( 'xopgi_mail_new_thread.group_new_thread'): raise exceptions.AccessDenied() if view_type == 'form': if (len(context.get('active_ids', [])) > 1 or not context.get('default_message_id', False)): raise exceptions.ValidationError( _('You should select one and only one message.')) result = super(NewThreadWizard, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) return result @api.multi def confirm(self): '''Create a new mail thread, post new message, remove original message if not leave_msg and open new thread on edit form. ''' thread_model = self.env[self.model_id] self.ensure_one() context = dict(wizard_id=self.id, new_thread_from_mail_msg=True, thread_model=self.model_id) rec_name = thread_model.fields_get().get( thread_model._rec_name or 'name', False) if rec_name and rec_name['type'] in ('char', 'text'): name = self.message_id.subject or self.message_id.display_name if name: context['default_%s' % (thread_model._rec_name or 'name')] = name return self.with_context(**context).get_thread_action()
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 AnalyticAccount(models.Model): _inherit = get_modelname(AccountAnalyticAccount) parent_id = fields.Many2one(comodel_name='account.analytic.account', string='Parent Analytic Account', select=2) child_ids = fields.One2many(comodel_name='account.analytic.account', inverse_name='parent_id', string='Child Accounts') @api.multi def check_recursion(self, parent=None): return super(AnalyticAccount, self)._check_recursion(parent=parent) _constraints = [ (check_recursion, 'Error! You cannot create recursive analytic accounts.', ['parent_id']), ]
def BoardValue(xml_id, model, module, **kwargs): '''A board value field. A value is a M2O field stored like as an XML_ID of the `model` and `module` provided. To be used within the model 'xopgi.board.config' to introduce further values or configuration. ''' module = module.rsplit('.', 1)[-1] # Allow module to be fully qualified # so you can say __name__ # noqa return fields.Many2one( model, compute=lambda self: self._compute_value(xml_id, module=module), inverse=lambda self: self._set_value(xml_id, model, module=module), default=lambda self: self._compute_value(xml_id, module=module), **kwargs )
class PurchaseOrder(models.Model): _inherit = models.get_modelname(PurchaseOrder) bid_date = fields.Datetime( string='Bid Received On', readonly=True, help="Date on which the bid was received" ) state = fields.Selection([ ('draft', 'Draft PO'), ('sent', 'RFQ Sent'), ('bid', 'Bid Received'), ('to approve', 'To Approve'), ('purchase', 'Purchase Order'), ('done', 'Done'), ('cancel', 'Cancelled')]) incoterm_id = fields.Many2one( 'stock.incoterms', string="Incoterm", help=("International Commercial Terms are a series of predefined " "commercial terms used in international transactions.") ) @api.multi def accion_bid_received(self): return self.write({'state': 'bid', 'bid_date': fields.Datetime.now()}) @api.multi def button_confirm(self): approve = self.browse() for order in self: if order.state in ['draft', 'sent', 'bid']: approve |= order # Odoo 9+ skips orders that are not in 'draft', or 'sent'. So # we trick it to process orders in state 'bid'. if order.state == 'bid': order.state = 'sent' return super(PurchaseOrder, approve).button_confirm()
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 BasicEvent(models.Model): _name = 'cdr.basic.event' _description = 'Basic CDR event' _inherits = {'cdr.system.event': 'event_id'} event_id = fields.Many2one('cdr.system.event', required=True, ondelete='cascade') interval = fields.Float( required=True, help='Time (in hours:minutes format) between evaluations.') time_to_wait = fields.Float( required=True, help='Time (in hours:minutes format) getting ' 'consecutive positive evaluations before raise.') times_to_raise = fields.Integer( help='Waiting time to launch an event while an evidence is true in a ' 'time interval') @api.depends('interval') def get_next_call(self): '''Compute the next evaluation date. ''' for event in self: if event.active and event.interval: event.next_call = datetime.now() + timedelta( hours=event.interval) else: event.next_call = False def update_event(self, value, cycle): '''Update the fields next call, state and action for an event. When an event is evaluated is necessary to update its values. ''' next_call = str2dt(cycle.create_date) + timedelta(hours=self.interval) # If the interval is less or equal zero that means that the event does # not wait any time to launch. if self.interval <= 0: times_to_raise = -1 else: times_to_raise = ((self.time_to_wait / self.interval) if not value else self.times_to_raise - 1) state = 'raising' if value and times_to_raise < 1 else 'not_raising' if self.state == 'raising': action = 'continue_raising' if state == 'raising' else 'stop_raising' else: action = 'raise' if state == 'raising' else 'do_nothing' values = dict(next_call=next_call, state=state, action=action) self.write(values) def evaluate(self, cycle): '''Evaluate the basic event in a evaluation cycle. ''' if isinstance(cycle, int): cycle = self.env['cdr.evaluation.cycle'].browse(cycle) try: value = self.event_id._evaluate() except Exception: logger.exception('Error evaluating event %s defined as: ', self.name, self.definition) return None else: self.update_event(value, cycle)
class AnalyticLine(models.Model): _inherit = get_modelname(AccountAnalyticLine) parent_account_id = fields.Many2one(related="account_id.parent_id", store=True, readonly=True)
class XopgiAccountAdvancementConfig(models.AbstractModel): _name = 'xopgi.account_adv.config.settings' @api.multi def get_account_types(self): '''Returns a dictionary of type opendict() with the system params: advanced_receivable_type_id and advanced_payable_type_id and their values. ''' res = opendict() get_param = self.env['ir.config_parameter'].get_param for param in DEFAULT_ACCOUNT_TYPES: value = safe_eval(str(get_param(param))) or False res[param] = value if self: setattr(self, param, value) return res @api.one def _set_account_types(self): '''Set the account types Receivable Advanced when fields advanced_receivable_type_id or advanced_payable_type_id change and update the filter in account settings with the new field values. ''' set_param = self.env['ir.config_parameter'].set_param for param in DEFAULT_ACCOUNT_TYPES: set_param(param, repr(getattr(self, param).id)) @api.multi def get_advanced_journal_types(self): '''Returns a dictionary of type opendict() with the system params: and their precollection_journal_type_id and prepayment_journal_type_id values. ''' res = opendict() get_param = self.env['ir.config_parameter'].get_param for param in DEFAULT_ADVANCED_JOURNAL_TYPES: value = safe_eval(str(get_param(param))) or False res[param] = value if self: setattr(self, param, value) return res @api.one def _set_advanced_journal_types(self): '''Set the advance journal types when fields precollection_journal_type_id or prepayment_journal_type_id change and update the filter in account settings with the new field values. ''' set_param = self.env['ir.config_parameter'].set_param for param in DEFAULT_ADVANCED_JOURNAL_TYPES: set_param(param, repr(getattr(self, param).id)) advanced_receivable_type_id = fields.Many2one( 'account.account.type', compute='get_account_types', inverse='_set_account_types', default=lambda self: self.get_account_types(). advanced_receivable_type_id, help='The type of pre-collection accounts.', ) advanced_payable_type_id = fields.Many2one( 'account.account.type', compute='get_account_types', inverse='_set_account_types', default=lambda self: self.get_account_types().advanced_payable_type_id, help='The type of the pre-payment accounts.', ) precollection_journal_type_id = fields.Many2one( 'account.journal', compute='get_advanced_journal_types', inverse='_set_advanced_journal_types', default=lambda self: self.get_advanced_journal_types(). precollection_journal_type_id, help='The type of journal for pre-collection accounts', ) prepayment_journal_type_id = fields.Many2one( 'account.journal', compute='get_advanced_journal_types', inverse='_set_advanced_journal_types', default=lambda self: self.get_advanced_journal_types(). prepayment_journal_type_id, help='The type of journal for pre-payment accounts', )
class Product(models.Model): _inherit = 'product.template' product_manager = fields.Many2one('res.users', string="Manager")
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 AccountAnalyticAccount(models.Model): _inherit = 'account.analytic.account' # The formulae for the commission to salesmen is based on a simple # projection from the desired sales margin (required_margin, max_margin) # to the commission margins salesmen earn per sale. # # The projection is simply a geometrical projection: required_margin # yields the minimal commission margin, the max_margin yields the maximal # commission margin. Accounts that fail to achieve the required_margin # yield 0% commission, accounts that surpass the max_margin yields just # the maximal commission margin. # # The projection follows the scheme below: # # min margin ----------X------------------ max margin # | # min com ----------V------------------ max comm # # X represents the actual margin, V represents the resultant commission # margin. # # V = min_comm * alpha + max_comm * (1 - alpha), where alpha is given by # the formula: # # alpha = (max_margin - X)/(max_margin - min_margin) # # if alpha < 0: alpha = 0 # Too much margin # # if V < min_comm: V = 0 # # Each account may have any desired required_margin and max_margin, # provided the required_margin is greater or equal of is parent account # and the max_margin is less or equal than the parent account. # # The account may have any desired min commission and max commission, no # restrictions are placed. # # By default, accounts take all these parameters from its parent. So it's # wise to only establish them at accounts that consolidate operational # branches. # required_margin = fields.Float( string='Required margin', help=('This is the minimum margin required for an operation. If 0 ' 'no commission will be calculated.'), required=False, default=0, track_visibility='onchange', digits=dp.get_precision('Account'), ) max_margin = fields.Float( string='Maximum margin', help='This is maximum margin allowed for an operation.', required=False, default=0, track_visibility='onchange', digits=dp.get_precision('Account'), ) min_commission_margin = fields.Float( string='Minimum commission margin', help='This is minimum margin for commissions.', required=False, default=0, track_visibility='onchange', digits=dp.get_precision('Account'), ) max_commission_margin = fields.Float( string='Maximum commission margin', help='This is maximum margin for commissions.', required=False, default=0, track_visibility='onchange', digits=dp.get_precision('Account'), ) current_required_margin = fields.Float( compute=_compute_from_branch('required_margin', 'current_required_margin', default=0), digits=dp.get_precision('Account'), ) current_max_margin = fields.Float( compute=_compute_from_branch('max_margin', 'current_max_margin', default=0), digits=dp.get_precision('Account'), ) current_min_comm = fields.Float( compute=_compute_from_branch('min_commission_margin', 'current_min_comm', default=0), digits=dp.get_precision('Account'), ) current_max_comm = fields.Float( compute=_compute_from_branch('max_commission_margin', 'current_max_comm', default=0), digits=dp.get_precision('Account'), ) percentage_margin = fields.Float( string='Margin %', help='Percentage margin related to credit.', compute='_compute_commission', digits=dp.get_precision('Account'), ) percentage_commission = fields.Float( string='Commission %', help='Percentage commission related to profit.', compute='_compute_commission', digits=dp.get_precision('Account'), ) commission = fields.Float( string='Commission', help='Commission related to profit.', compute='_compute_commission', digits=dp.get_precision('Account'), ) invoiced = fields.Float( string='Invoiced', help=('The amount invoiced for this account, ' 'discounting refunds.'), compute='_compute_invoiced', digits=dp.get_precision('Account'), store=True, compute_sudo=True ) expended = fields.Float( string='Expended', help=('The amount expended for this account, ' 'discounting refunds.'), compute='_compute_invoiced', digits=dp.get_precision('Account'), store=True, compute_sudo=True ) amount_undefined = fields.Float( string='Undefined', help=('Total that cannot be accounted as invoiced or expended because ' 'it is not attached to an invoice'), compute='_compute_invoiced', digits=dp.get_precision('Account'), store=True, compute_sudo=True ) self_balance = fields.Float( string='Balance', help=('Self balance'), compute='_compute_invoiced', digits=dp.get_precision('Account'), store=True, compute_sudo=True ) primary_salesperson_id = fields.Many2one( "res.users", string="Salesperson", help="Primary salesperson in operation", compute="_compute_primary_salesperson", store=True ) supplier_invoice_id = fields.Many2one('account.invoice', ondelete='set null') # TODO: Ensure only groups='base.group_sale_manager' can update the # commission margins. So far, only the goodwill of ignorance may save us. @api.depends('line_ids') def _compute_invoiced(self): for record in self: invoiced = expended = undefined = 0 for line in record.line_ids: if line.move_id and line.move_id.invoice_id: if line.move_id.invoice_id.type in INCOME_INVOICE_TYPES: invoiced += line.amount else: expended -= line.amount else: undefined += line.amount record.invoiced = invoiced record.expended = expended record.amount_undefined = undefined record.self_balance = invoiced - expended + undefined @api.depends('invoiced', 'self_balance') def _compute_commission(self): for record in self: margin, comm_percent, balance = _compute_margin_commission(record) record.percentage_margin = margin * 100 record.percentage_commission = comm_percent * 100 record.commission = comm_percent * balance @api.depends('line_ids.move_id.invoice_id.user_id') def _compute_primary_salesperson(self): for account in self: if not account.active: account.primary_salesperson_id = False else: user_id = next( (line.move_id.invoice_id.user_id for line in account.line_ids if line and line.move_id and line.move_id.invoice_id if line.move_id.invoice_id.type in INCOME_INVOICE_TYPES if line.move_id.invoice_id.user_id), None ) if user_id: account.primary_salesperson_id = user_id else: account.primary_salesperson_id = False @api.constrains('required_margin', 'max_margin') def _validate_margins(self): for record in self: required_margin = record.required_margin max_margin = record.max_margin if bool(required_margin) != bool(max_margin): # Either both are set or both are unset raise ValidationError('You must set both required_margin ' 'and max_margin or none.') if required_margin < 0 or required_margin > 100: raise_validation_error('required minimum margin') if max_margin < 0 or max_margin > 100: raise_validation_error('maximum allowed margin') if required_margin and max_margin and \ required_margin >= max_margin: raise ValidationError('required margin must be less that ' 'max margin.') parent = record.parent_id if parent and parent.id: if parent.required_margin and required_margin and \ required_margin < parent.required_margin: raise ValidationError( 'You cannot lower the required margin') if parent.max_margin and max_margin and \ max_margin > parent.max_margin: raise ValidationError( 'You cannot raise the maximum margin') # TODO: If the children enter in violation what to do? # for child in record.complete_child_ids: # child._validate_margins() @api.constrains('min_commission_margin', 'max_commission_margin') def _validate_commission_margins(self): for record in self: min_comm = record.min_commission_margin max_comm = record.max_commission_margin if bool(min_comm) != bool(max_comm): # Either both are set or both are unset raise ValidationError('You must set both min commission ' 'and max commission or none.') if min_comm < 0 or min_comm > 100: raise_validation_error('minimum commission margin') if max_comm < 0 or max_comm > 100: raise_validation_error('maximum commission margin') if min_comm and max_comm and min_comm >= max_comm: raise ValidationError('min commission must be less that ' 'max commission.')